Recently I took Warp for a testdrive. I have a couple of friends who have a eco-glamping and where in need of a website. So perfect opportunity to see the current state of AI in webdev.
And colour me impressed. I was able to create a one pager powered by a tailored CMS. The frontend is plain html/css/js powered by json files that are managed by the php backend.
Now as an additional challenge I wanted to make sure the content was also source controlled. Every *.json change and updated file in /images (the path where user uploaded files are stored) should be stored in git.
It took me a couple of prompts to end up with a php scripts that uses the github content api to check the status and if necessary update the content files.
Running this as a scheduled task on a Plesk interface from a cheap hosting solution.
`
<?php
/**
* GitHub Sync Script for DenBerendries - Using Contents API
*
* This script uses the GitHub Contents API which handles encoding automatically
* and syncs both JSON files in root and images in /images directory
*/
// Configuration
$GITHUB_TOKEN = '???';
$GITHUB_USERNAME = 'timgeyssens';
$GITHUB_REPO = 'DenBerendries';
$BRANCH = 'main';
$COMMIT_MESSAGE = 'Auto commit from server on ' . date('Y-m-d H:i:s');
// Set content type to JSON
header('Content-Type: application/json');
// Check if script is called via HTTP or CLI
$isHttp = (php_sapi_name() !== 'cli');
// Function to make GitHub API requests
function githubApiRequest($url, $token, $method = 'GET', $data = null) {
$ch = curl_init();
$headers = [
'Authorization: token ' . $token,
'User-Agent: PHP-GitHub-Sync-Script',
'Content-Type: application/json',
];
curl_setopt($ch, CURLOPT_URL, 'https://api.github.com' . $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
} elseif ($method === 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
} elseif ($method === 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['code' => $httpCode, 'body' => json_decode($response, true)];
}
// Function to find JSON files in root and images in /images directory
function findFilesToSync() {
$files = [];
$basePath = realpath('.');
// Supported image extensions
$imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp'];
// 1. Find JSON files in root directory
$rootIterator = new DirectoryIterator($basePath);
foreach ($rootIterator as $file) {
if ($file->isFile() && !$file->isDot()) {
$extension = strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
// Check if it's a JSON file
if ($extension === 'json') {
// Get relative path from the base directory
$relativePath = substr($file->getRealPath(), strlen($basePath) + 1);
// Normalize path separators for GitHub
$relativePath = str_replace('\\', '/', $relativePath);
$files[] = $relativePath;
}
}
}
// 2. Find image files in /images directory
$imagesPath = $basePath . DIRECTORY_SEPARATOR . 'images';
if (is_dir($imagesPath)) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($imagesPath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$extension = strtolower(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
// Check if it's an image file
if (in_array($extension, $imageExtensions)) {
// Get relative path from the base directory
$relativePath = substr($file->getRealPath(), strlen($basePath) + 1);
// Normalize path separators for GitHub
$relativePath = str_replace('\\', '/', $relativePath);
$files[] = $relativePath;
}
}
}
}
return $files;
}
// Function to get file SHA from GitHub
function getFileShaFromGitHub($token, $username, $repo, $branch, $filePath) {
$url = "/repos/$username/$repo/contents/$filePath?ref=$branch";
$response = githubApiRequest($url, $token);
if ($response['code'] === 200) {
return $response['body']['sha'];
}
return null;
}
// Function to sync files using Contents API
function syncToGitHub($token, $username, $repo, $branch, $commitMessage) {
$result = [];
// Find all files to sync
$allFiles = findFilesToSync();
$result['all_files'] = count($allFiles);
if (count($allFiles) === 0) {
return ['success' => true, 'message' => 'No files found to sync'];
}
$changedFiles = [];
$skippedFiles = [];
foreach ($allFiles as $file) {
if (file_exists($file)) {
$localContent = file_get_contents($file);
$localSha = sha1("blob " . strlen($localContent) . "\0" . $localContent);
// Get the remote SHA
$remoteSha = getFileShaFromGitHub($token, $username, $repo, $branch, $file);
// If file doesn't exist in remote or SHA is different, it's changed
if (!$remoteSha || $remoteSha !== $localSha) {
$changedFiles[] = $file;
} else {
$skippedFiles[] = $file;
}
}
}
$result['changed_files'] = count($changedFiles);
$result['skipped_files'] = count($skippedFiles);
$result['files'] = $changedFiles;
// If no files changed, return early without creating a commit
if (count($changedFiles) === 0) {
return ['success' => true, 'message' => 'No changes to commit', 'action' => 'none'];
}
// Update each changed file
$successCount = 0;
$errorCount = 0;
$errors = [];
foreach ($changedFiles as $file) {
if (file_exists($file)) {
$content = file_get_contents($file);
$sha = getFileShaFromGitHub($token, $username, $repo, $branch, $file);
$data = [
'message' => $commitMessage,
'content' => base64_encode($content),
'branch' => $branch
];
// If file exists, include the SHA for update
if ($sha) {
$data['sha'] = $sha;
}
$response = githubApiRequest("/repos/$username/$repo/contents/$file", $token, 'PUT', $data);
if ($response['code'] === 200 || $response['code'] === 201) {
$successCount++;
} else {
$errorCount++;
$errors[] = "Failed to update $file: " . json_encode($response['body']);
}
}
}
if ($errorCount > 0) {
$result['success'] = false;
$result['message'] = "Completed with $successCount successes and $errorCount errors";
$result['errors'] = $errors;
} else {
$result['success'] = true;
$result['message'] = "Sync completed successfully. Updated $successCount files.";
}
$result['action'] = 'pushed';
return $result;
}
// Handle the request
try {
$startTime = microtime(true);
// If called via HTTP with a custom message
if ($isHttp && isset($_GET['message'])) {
$COMMIT_MESSAGE = $_GET['message'];
}
$result = syncToGitHub($GITHUB_TOKEN, $GITHUB_USERNAME, $GITHUB_REPO, $BRANCH, $COMMIT_MESSAGE);
$result['execution_time'] = round(microtime(true) - $startTime, 2) . ' seconds';
if ($isHttp) {
echo json_encode($result, JSON_PRETTY_PRINT);
} else {
// CLI output
if ($result['success']) {
if ($result['action'] === 'pushed') {
echo "β
Sync completed successfully!\n";
echo "π All files: " . $result['all_files'] . "\n";
echo "π Changed files: " . $result['changed_files'] . "\n";
echo "π Skipped files: " . $result['skipped_files'] . "\n";
echo "β±οΈ Execution time: " . $result['execution_time'] . "\n";
// List files if there are not too many
if ($result['changed_files'] > 0 && $result['changed_files'] <= 20) {
echo "\nπ Files synced:\n";
foreach ($result['files'] as $file) {
echo " - $file\n";
}
}
} else {
echo "β
No changes to commit. Everything is up to date.\n";
echo "π Files checked: " . $result['all_files'] . "\n";
echo "β±οΈ Execution time: " . $result['execution_time'] . "\n";
}
} else {
echo "β Sync completed with errors:\n";
echo "π Successes: " . ($result['changed_files'] - count($result['errors'])) . "\n";
echo "π Errors: " . count($result['errors']) . "\n";
echo "β±οΈ Execution time: " . $result['execution_time'] . "\n";
foreach ($result['errors'] as $error) {
echo "β $error\n";
}
}
}
} catch (Exception $e) {
$error = ['success' => false, 'message' => 'Exception: ' . $e->getMessage()];
if ($isHttp) {
echo json_encode($error, JSON_PRETTY_PRINT);
} else {
echo "β Exception: " . $e->getMessage() . "\n";
}
}
// If called via HTTP, add some HTML styling
if ($isHttp) {
echo <<<HTML
<!DOCTYPE html>
<html>
<head>
<title>GitHub Sync Result</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.success {
color: #2ecc71;
font-weight: bold;
}
.error {
color: #e74c3c;
font-weight: bold;
}
.info {
color: #3498db;
font-weight: bold;
}
pre {
background: #f8f8f8;
padding: 15px;
border-radius: 5px;
overflow: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>GitHub Sync Result</h1>
<p>Repository: <a href="https://github.com/$GITHUB_USERNAME/$GITHUB_REPO" target="_blank">$GITHUB_USERNAME/$GITHUB_REPO</a></p>
<p>Scanning: JSON files in root directory and images in /images directory</p>
<p>Commit message: $COMMIT_MESSAGE</p>
</div>
</body>
</html>
HTML;
}
`
Top comments (0)