Passed
Pull Request — master (#24)
by Ronan
09:51
created

Github::getHeadInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 17
c 2
b 0
f 0
dl 0
loc 23
ccs 0
cts 22
cp 0
rs 9.7
cc 1
nc 1
nop 2
crap 2
1
<?php
2
3
namespace App\Provider;
4
5
use App\Builder;
6
use App\Facades\Log;
7
use App\Facades\Settings;
8
use App\Model\Deployment;
9
use App\Model\Project;
10
use App\Provider\AbstractProvider;
11
use App\Provider\ProviderInterface;
12
use Closure;
13
use Exception;
14
use GuzzleHttp\ClientInterface;
15
use Psr\Http\Message\StreamInterface;
16
use Ronanchilvers\Foundation\Config;
17
use Ronanchilvers\Utility\Str;
18
use RuntimeException;
19
use Symfony\Component\Process\Exception\ProcessFailedException;
20
use Symfony\Component\Process\Process;
21
use Symfony\Component\Yaml\Yaml;
22
23
/**
24
 * Github source control provider
25
 *
26
 * @author Ronan Chilvers <[email protected]>
27
 */
28
class Github extends AbstractProvider implements ProviderInterface
29
{
30
    /**
31
     * @var array
32
     */
33
    protected $typesHandled = ['github'];
34
35
    /**
36
     * @var string
37
     */
38
    protected $headUrl = 'https://api.github.com/repos/{repository}/git/refs/heads/{branch}';
39
40
    /**
41
     * @var string
42
     */
43
    protected $commitUrl = 'https://api.github.com/repos/{repository}/commits/{sha}';
44
45
    /**
46
     * @var string
47
     */
48
    protected $downloadUrl = 'https://api.github.com/repos/{repository}/tarball/{sha}';
49
50
    /**
51
     * @var string
52
     */
53
    protected $configUrl = 'https://api.github.com/repos/{repository}/contents/deploy.yaml?ref={sha}';
54
55
    /**
56
     * @var string
57
     */
58
    protected $repoUrl = 'https://github.com/{repository}';
59
60
    /**
61
     * @var string
62
     */
63
    protected $branchUrl = 'https://github.com/{repository}/tree/{branch}';
64
65
    /**
66
     * @var string
67
     */
68
    protected $shaUrl = 'https://github.com/{repository}/commit/{sha}';
69
70
    /**
71
     * @see \App\Provider\ProviderInterface::getHeadInfo()
72
     */
73
    public function getHeadInfo(string $repository, string $branch)
74
    {
75
        $params = [
76
            'repository' => $repository,
77
            'branch'     => $branch,
78
        ];
79
        $url = Str::moustaches(
80
            $this->headUrl,
81
            $params
82
        );
83
        $data = $this->getJSON($url);
84
        $params['sha'] = $data['object']['sha'];
85
        $url = Str::moustaches(
86
            $this->commitUrl,
87
            $params
88
        );
89
        $data = $this->getJSON($url);
90
91
        return [
92
            'sha'       => $data['sha'],
93
            'author'    => $data['commit']['author']['email'],
94
            'committer' => $data['commit']['committer']['email'],
95
            'message'   => $data['commit']['message'],
96
        ];
97
    }
98
99
    /**
100
     * @see \App\Provider\ProviderInterface::download()
101
     */
102
    public function download(Project $project, Deployment $deployment, $directory, Closure $closure = null)
103
    {
104
        $params = [
105
            'repository' => $project->repository,
106
            'sha'        => $deployment->sha,
107
        ];
108
        $url = Str::moustaches(
109
            $this->downloadUrl,
110
            $params
111
        );
112
113
        // Download the code tarball
114
        $filename = tempnam('/tmp', 'deploy-' . $params['sha'] . '-');
115
        if (!$handle = fopen($filename, "w")) {
116
            $closure(
117
                'error',
118
                implode("\n", [
119
                    "Unable to open temporary download file: {$filename}"
120
                ])
121
            );
122
            throw new RuntimeException('Unable to open temporary file');
123
        }
124
        $curl = $this->getCurlHandle($url);
125
        curl_setopt_array($curl, [
126
            CURLOPT_FOLLOWLOCATION => true,
127
            CURLOPT_FILE           => $handle
128
        ]);
129
        if (false === curl_exec($curl)) {
130
            $closure(
131
                'error',
132
                implode("\n", [
133
                    'Error downloading codebase',
134
                    "Filename - {$filename}",
135
                    "CURL Error - (" . curl_errno($curl) . ') ' . curl_error($curl),
136
                ])
137
            );
138
            throw new RuntimeException(curl_errno($curl) . ' - ' . curl_error($curl));
139
        }
140
        $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
141
        curl_close($curl);
142
        fclose($handle);
143
        if ($statusCode != 200) {
144
            $closure(
145
                'error',
146
                implode("\n", [
147
                    'Error downloading codebase',
148
                    "Filename - {$filename}",
149
                    "Status code - {$statusCode}",
150
                ])
151
            );
152
            throw new RuntimeException('Failed to download codebase - ' . $statusCode);
153
        }
154
155
        // Make sure the deployment download directory exists
156
        if (!is_dir($directory)) {
157
            $mode = Settings::get('build.chmod.default_folder', Builder::MODE_DEFAULT);
158
            $closure('info', "Creating deployment directory: {$directory}");
159
            if (!mkdir($directory, $mode, true)) {
160
                $closure('error', "Failed to create deployment directory: {$directory}");
161
                throw new RuntimeException(
162
                    'Unable to create build directory at ' . $directory
163
                );
164
            }
165
        }
166
167
        // Decompress the archive into the download directory
168
        $tar     = Settings::get('binary.tar', '/bin/tar');
169
        $command = "{$tar} --strip-components=1 -xzf {$filename} -C {$directory}";
170
        $closure('info', "Unpacking codebase tarball: {$command}");
171
        $process = new Process(explode(' ', $command));
172
        $process->run();
173
        if (!$process->isSuccessful()) {
174
            $closure(
175
                'error',
176
                implode("\n", [
177
                    "Unpack failed: {$command}",
178
                    $process->getErrorOutput(),
179
                ])
180
            );
181
            throw new ProcessFailedException($process);
182
        }
183
184
        // Remove the downloaded archive
185
        if (!unlink($filename)) {
186
            $closure(
187
                'error',
188
                implode("\n", [
189
                    'Unable to remove tarball after unpacking',
190
                    $process->getOutput(),
191
                ])
192
            );
193
            throw new RuntimeException('Unable to remove local code archive');
194
        }
195
196
        return true;
197
    }
198
199
    /**
200
     * @see \App\Provider\ProviderInterface::scanConfiguration()
201
     */
202
    public function scanConfiguration(Project $project, Deployment $deployment, Closure $closure = null)
203
    {
204
        $params = [
205
            'repository' => $project->repository,
206
            'sha'        => $deployment->sha,
207
        ];
208
        $url = Str::moustaches(
209
            $this->configUrl,
210
            $params
211
        );
212
        $closure('info', "Querying Github API: {$url}");
213
        $curl = $this->getCurlHandle($url);
214
        $json = curl_exec($curl);
215
        if (false === $json || !is_string($json)) {
216
            $closure(
217
                'error',
218
                implode("\n", [
219
                    'Github API request failed',
220
                    "API URL - {$url}",
221
                    "CURL Error - (" . curl_errno($curl) . ') ' . curl_error($curl)
222
                ])
223
            );
224
            throw new RuntimeException("(" . curl_errno($curl) . ') ' . curl_error($curl));
225
        }
226
        $info = curl_getinfo($curl);
227
        if (404 == $info['http_code']) {
228
            $closure(
229
                'info',
230
                'No deployment configuration found in repository - using defaults'
231
            );
232
            Log::debug('Remote configuration file not found', [
233
                'project' => $project->toArray(),
234
            ]);
235
            return;
236
        }
237
        $data = json_decode($json, true);
238
        if (!$data || !isset($data['content'])) {
239
            $closure(
240
                'error',
241
                implode("\n", [
242
                    'Failed to parse Github response json',
243
                    "API URL - {$url}",
244
                    "JSON - " . $json
245
                ])
246
            );
247
            Log::debug('Remote configuration file could not be read', [
248
                'project' => $project->toArray(),
249
            ]);
250
            return;
251
        }
252
        $yaml = base64_decode($data['content']);
253
        try {
254
            $yaml = Yaml::parse($yaml);
255
            $closure(
256
                'info',
257
                implode("\n", [
258
                    'YAML deployment configuration read successfully',
259
                    "JSON - " . $json
260
                ])
261
            );
262
        } catch (Exception $ex) {
263
            $closure(
264
                'error',
265
                implode("\n", [
266
                    'Unable to parse YAML deployment configuration',
267
                    "Exception - " . $ex->getMessage(),
268
                ])
269
            );
270
            Log::error('Unable to parse YAML deployment configuration', [
271
                'project'   => $project->toArray(),
272
                'exception' => $ex,
273
            ]);
274
            return;
275
        }
276
277
        return new Config($yaml);
278
    }
279
280
    /**
281
     * Get a curl handle
282
     *
283
     * @param string $url
284
     * @return resource
285
     * @author Ronan Chilvers <[email protected]>
286
     */
287
    protected function getCurlHandle($url)
288
    {
289
        $curl = parent::getCurlHandle($url);
290
        curl_setopt_array($curl, [
291
            CURLOPT_HTTPHEADER     => [
292
                "Authorization: token {$this->token}"
293
            ],
294
        ]);
295
296
        return $curl;
297
    }
298
}
299