build-release.php

main 150 lines · 5.2 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2
3if (PHP_SAPI !== 'cli') {
4 http_response_code(403);
5 exit(1);
6}
7
8$root = dirname(__DIR__);
9$metadataFile = __DIR__ . '/release-metadata.json';
10$metadata = json_decode((string)file_get_contents($metadataFile), true);
11$version = (string)($argv[1] ?? ($metadata['version'] ?? ''));
12$outputRoot = rtrim((string)($argv[2] ?? ($root . '/release')), '/\\');
13$current = (string)require $root . '/engine/version.php';
14
15if (!is_array($metadata) || $version === '' || $version !== (string)($metadata['version'] ?? '') || $version !== $current) {
16 fwrite(STDERR, "The version must match tools/release-metadata.json and engine/version.php.\n");
17 exit(1);
18}
19if (!class_exists('ZipArchive') || !function_exists('openssl_sign')) {
20 fwrite(STDERR, "PHP Zip and OpenSSL are required.\n");
21 exit(1);
22}
23
24$signingDirectory = $outputRoot . '/.signing';
25$privateKeyFile = $signingDirectory . '/private.pem';
26$publicKeyFile = $root . '/engine/update-public.pem';
27if (!is_dir($signingDirectory) && !mkdir($signingDirectory, 0700, true) && !is_dir($signingDirectory)) {
28 fwrite(STDERR, "The signing directory cannot be created.\n");
29 exit(1);
30}
31
32if (!is_file($privateKeyFile)) {
33 fwrite(STDERR, "The private signing key is missing. Run tools/generate-release-key.ps1 once.\n");
34 exit(1);
35}
36
37$privatePem = (string)file_get_contents($privateKeyFile);
38$key = openssl_pkey_get_private($privatePem);
39$details = $key !== false ? openssl_pkey_get_details($key) : false;
40if ($key === false || !is_array($details) || !is_file($publicKeyFile) || trim((string)file_get_contents($publicKeyFile)) !== trim((string)$details['key'])) {
41 fwrite(STDERR, "The private key does not match engine/update-public.pem.\n");
42 exit(1);
43}
44
45$releaseDirectory = $outputRoot . '/' . $version;
46if (!is_dir($releaseDirectory) && !mkdir($releaseDirectory, 0755, true) && !is_dir($releaseDirectory)) {
47 fwrite(STDERR, "The release directory cannot be created.\n");
48 exit(1);
49}
50if (is_file(__DIR__ . '/RELEASE.md')) {
51 copy(__DIR__ . '/RELEASE.md', $outputRoot . '/README.md');
52}
53
54$allowedDirectories = array('admin', 'api', 'assets', 'engine', 'locale', 'SQL', 'vendor');
55$excludedPrefixes = array('engine/cache/', 'engine/img/theme/', 'engine/update/');
56$files = array();
57
58$accept = static function (string $relative) use ($excludedPrefixes): bool {
59 $relative = str_replace('\\', '/', $relative);
60 foreach ($excludedPrefixes as $prefix) {
61 if (str_starts_with($relative, $prefix)) {
62 return false;
63 }
64 }
65 return !str_ends_with($relative, '.znote-update');
66};
67
68foreach ($allowedDirectories as $directory) {
69 $base = $root . '/' . $directory;
70 if (!is_dir($base)) {
71 continue;
72 }
73 $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base, FilesystemIterator::SKIP_DOTS));
74 foreach ($iterator as $item) {
75 if (!$item->isFile() || $item->isLink()) {
76 continue;
77 }
78 $relative = str_replace('\\', '/', substr($item->getPathname(), strlen($root) + 1));
79 if ($accept($relative)) {
80 $files[$relative] = $item->getPathname();
81 }
82 }
83}
84
85foreach (glob($root . '/*.php') ?: array() as $file) {
86 $name = basename($file);
87 if (!in_array(strtolower($name), array('config.php', 'config.local.php'), true)) {
88 $files[$name] = $file;
89 }
90}
91foreach (array('.htaccess', 'composer.json', 'composer.lock', 'LICENSE', 'README.md', 'config.countries.php') as $name) {
92 if (is_file($root . '/' . $name)) {
93 $files[$name] = $root . '/' . $name;
94 }
95}
96ksort($files);
97
98$packageName = 'znotex-core-' . $version . '.zip';
99$packageFile = $releaseDirectory . '/' . $packageName;
100$zip = new ZipArchive();
101if ($zip->open($packageFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
102 fwrite(STDERR, "The release ZIP cannot be created.\n");
103 exit(1);
104}
105$hashes = array();
106foreach ($files as $relative => $file) {
107 if (!$zip->addFile($file, $relative)) {
108 $zip->close();
109 fwrite(STDERR, "A file cannot be added: " . $relative . "\n");
110 exit(1);
111 }
112 $hashes[$relative] = hash_file('sha256', $file);
113}
114$zip->close();
115
116$manifest = $metadata;
117$manifest['schema'] = 1;
118$manifest['project'] = 'ZnoteX';
119$manifest['version'] = $version;
120$manifest['published_at'] = gmdate(DATE_ATOM);
121$manifest['package'] = array(
122 'name' => $packageName,
123 'sha256' => hash_file('sha256', $packageFile),
124 'size' => filesize($packageFile),
125);
126$manifest['installation'] = array(
127 'mode' => 'signed-core-update',
128 'backup' => true,
129 'rollback' => 'files',
130 'database_policy' => 'expand-only'
131);
132$manifest['protected_paths'] = array('config.php', 'config.local.php', 'plugins/', 'layouts/', 'engine/cache/', 'engine/img/theme/', 'engine/update/', 'install/', 'release/');
133$manifest['links'] = array(
134 'repository' => 'https://github.com/Open-Games-Community/ZnoteX',
135 'release' => 'https://github.com/Open-Games-Community/ZnoteX/releases/tag/v' . $version
136);
137$manifest['files'] = $hashes;
138
139$json = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
140if (!openssl_sign($json, $signature, $key, OPENSSL_ALGO_SHA256)) {
141 fwrite(STDERR, "The release manifest cannot be signed.\n");
142 exit(1);
143}
144file_put_contents($releaseDirectory . '/update.json', $json, LOCK_EX);
145file_put_contents($releaseDirectory . '/update.json.sig', base64_encode($signature) . "\n", LOCK_EX);
146
147echo $releaseDirectory . PHP_EOL;
148echo $packageName . PHP_EOL;
149echo count($hashes) . " files\n";
150
Top