Skip to content

Update eew-info.json #106

Update eew-info.json

Update eew-info.json #106

Workflow file for this run

name: Track Repository Stats
on:
pull_request:
types:
- closed
jobs:
track-stats:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
- name: Get changed files
id: changed-files
run: |
echo "Getting changed files..."
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}
echo "base_sha=$BASE_SHA" >> $GITHUB_ENV
echo "head_sha=$HEAD_SHA" >> $GITHUB_ENV
CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA)
echo "Changed files: $CHANGED_FILES"
if ! echo "$CHANGED_FILES" | grep -q "^infos/"; then
echo "No changes in infos/ directory. Skipping workflow."
exit 0
fi
# Save changed files for later use
echo "$CHANGED_FILES" > changed_files.txt
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: |
npm install axios fs-extra crypto child_process moment-timezone
- name: Verify plugins
run: |
cat > verify_plugin.js <<'EOL'
const fs = require('fs-extra');
const path = require('path');
const axios = require('axios');
const crypto = require('crypto');
const { execSync } = require('child_process');
class Colors {
static HEADER = '\x1b[95m';
static BLUE = '\x1b[94m';
static GREEN = '\x1b[92m';
static YELLOW = '\x1b[93m';
static RED = '\x1b[91m';
static RESET = '\x1b[0m';
}
class PluginVerifier {
constructor(officialKey) {
this.officialKey = officialKey;
}
normalizeContent(content) {
return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
}
readFileWithDetection(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return this.normalizeContent(content);
} catch (err) {
console.error(`Error reading file ${filePath}: ${err}`);
return '';
}
}
getAllFiles(directory, baseDir = null) {
if (!baseDir) baseDir = directory;
const results = {};
fs.readdirSync(directory).forEach(item => {
if (item.startsWith('.') || item === 'signature.json') return;
const fullPath = path.join(directory, item);
if (fs.statSync(fullPath).isDirectory()) {
Object.assign(results, this.getAllFiles(fullPath, baseDir));
} else {
const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
results[relPath] = this.readFileWithDetection(fullPath);
}
});
return results;
}
verify(pluginDir) {
try {
const signaturePath = path.join(pluginDir, 'signature.json');
if (!fs.existsSync(signaturePath)) {
return { valid: false, error: 'Missing signature.json' };
}
const signatureData = JSON.parse(fs.readFileSync(signaturePath, 'utf8'));
console.log(`${Colors.YELLOW}Signature data loaded:${Colors.RESET}`);
console.log(JSON.stringify(signatureData, null, 2));
const { fileHashes, signature, keyId } = signatureData;
if (!fileHashes || !signature) {
return { valid: false, error: 'Invalid signature data format' };
}
console.log(`\n${Colors.BLUE}Verifying files...${Colors.RESET}`);
const allFiles = this.getAllFiles(pluginDir);
console.log(`Found ${Object.keys(allFiles).length} files to verify:`);
Object.keys(allFiles).sort().forEach(file => console.log(` ${file}`));
console.log(`\n${Colors.BLUE}Checking file hashes...${Colors.RESET}`);
for (const [filePath, content] of Object.entries(allFiles)) {
if (filePath === 'trem.json') continue;
if (!(filePath in fileHashes)) {
return { valid: false, error: `Extra file: ${filePath}` };
}
const actualHash = crypto.createHash('sha256')
.update(content)
.digest('hex');
console.log(`File: ${filePath}`);
console.log(` Expected: ${fileHashes[filePath]}`);
console.log(` Actual: ${actualHash}`);
if (actualHash !== fileHashes[filePath]) {
return { valid: false, error: `Modified file: ${filePath}` };
}
}
console.log(`\n${Colors.BLUE}Verifying signature...${Colors.RESET}`);
console.log(`Using key ID: ${keyId || 'official'}`);
const message = Buffer.from(JSON.stringify(fileHashes, Object.keys(fileHashes).sort()));
const signatureBuffer = Buffer.from(signature, 'base64');
const verify = crypto.createVerify('SHA256');
verify.update(message);
const isValid = verify.verify(this.officialKey, signatureBuffer);
if (isValid) {
return { valid: true, error: null, keyId: keyId || 'official' };
}
return { valid: false, error: 'Invalid signature', keyId: keyId || 'official' };
} catch (err) {
console.error(`${Colors.RED}Verification error: ${err}${Colors.RESET}`);
return { valid: false, error: err.message };
}
}
}
async function downloadAndVerifyPlugin(org, repo, pluginName, verifier) {
console.log(`${Colors.BLUE}Downloading latest release from ${org}/${repo}...${Colors.RESET}`);
try {
const apiUrl = `https://api.github.com/repos/${org}/${repo}/releases/latest`;
const { data: releaseData } = await axios.get(apiUrl);
const tremFile = `${pluginName}.trem`;
const asset = releaseData.assets.find(a => a.name === tremFile);
if (!asset) {
throw new Error(`Could not find ${tremFile} in latest release`);
}
const response = await axios.get(asset.browser_download_url, {
responseType: 'arraybuffer'
});
const tempDir = fs.mkdtempSync('plugin-');
const tremPath = path.join(tempDir, tremFile);
const pluginDir = path.join(tempDir, 'plugin');
fs.writeFileSync(tremPath, response.data);
console.log(`${Colors.GREEN}Downloaded ${tremFile}${Colors.RESET}`);
fs.mkdirSync(pluginDir, { recursive: true });
execSync(`unzip ${tremPath} -d ${pluginDir}`);
const result = verifier.verify(pluginDir);
fs.removeSync(tempDir);
if (result.valid) {
console.log(`${Colors.GREEN}Plugin verification successful!${Colors.RESET}`);
return true;
} else {
console.log(`${Colors.RED}Plugin verification failed: ${result.error}${Colors.RESET}`);
return false;
}
} catch (err) {
console.error(`${Colors.RED}Error: ${err}${Colors.RESET}`);
return false;
}
}
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzQn1ouv0mfzVKJevJiq+
6rV9mwCEvQpauQ2QNjy4TiwhqzqNiOPwpM3qo+8+3Ld+DUhzZzSzyx894dmJGlWQ
wNss9Vs5/gnuvn6PurNXC42wkxY6Dmsnp/M6g08iqGXVcM6ZWmvCZ3BzBvwExxRR
09KxHZVhwoMcF5Kp9l/hNZqXRgYMn3GLt+m78Hr+ZUjHiF8K9UH2TPxKRa/4ttPX
6nDBZxZUCwFD7Zh6RePg07JDbO5fI/UYrqZYyDPK8w9xdXtke9LbdXmMuuk/x57h
foRArUkhPvUk/77mxo4++3EFnTUxYMnQVuMkDaYNRu7w83abUuhsjNlL/es24HSm
lwIDAQAB
-----END PUBLIC KEY-----`;
if (process.argv.length !== 5) {
console.log(`Usage: ${process.argv[1]} <org> <repo> <plugin_name>`);
process.exit(1);
}
const verifier = new PluginVerifier(publicKey);
downloadAndVerifyPlugin(process.argv[2], process.argv[3], process.argv[4], verifier)
.then(success => process.exit(success ? 0 : 1));
EOL
# Read and process each changed JSON file
while IFS= read -r file; do
if [[ $file == infos/* ]] && [[ $file == *.json ]]; then
echo "Processing $file..."
# Extract plugin info from JSON
PLUGIN_NAME=$(jq -r '.name' "$file")
REPO_LINK=$(jq -r '.link' "$file")
# Parse GitHub org and repo from link
if [[ $REPO_LINK =~ github.com/([^/]+)/([^/]+)$ ]]; then
ORG="${BASH_REMATCH[1]}"
REPO="${BASH_REMATCH[2]}"
echo "Verifying plugin: $PLUGIN_NAME from $ORG/$REPO"
node verify_plugin.js "$ORG" "$REPO" "$PLUGIN_NAME"
if [ $? -ne 0 ]; then
echo "Plugin verification failed for $PLUGIN_NAME"
exit 1
fi
else
echo "Invalid repository link format in $file"
exit 1
fi
fi
done < changed_files.txt
- name: Fetch repository stats
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
cat > fetch_stats.js <<'EOL'
const fs = require('fs-extra');
const axios = require('axios');
const { execSync } = require('child_process');
const moment = require('moment-timezone');
async function getChangedFiles() {
const baseSha = `${{ github.event.pull_request.base.sha }}`;
const headSha = `${{ github.event.pull_request.head.sha }}`;
const cmd = `git diff --name-only ${baseSha} ${headSha}`;
const result = execSync(cmd).toString();
return result.trim().split('\n').filter(f => f.startsWith('infos/') && f.endsWith('.json'));
}
async function getRepoStats(owner, repo, pluginInfo) {
const headers = {
Authorization: `token ${process.env.GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json'
};
try {
const baseUrl = `https://api.github.com/repos/${owner}/${repo}`;
const [repoResponse, releasesResponse] = await Promise.all([
axios.get(baseUrl, { headers }),
axios.get(`${baseUrl}/releases`, { headers })
]);
const releases = releasesResponse.data;
let totalDownloads = 0;
const releaseStats = releases.map(release => {
const downloads = release.assets.reduce((sum, asset) => sum + asset.download_count, 0);
totalDownloads += downloads;
return {
tag_name: release.tag_name,
name: release.name,
downloads,
published_at: release.published_at
};
});
return {
name: pluginInfo.name,
version: pluginInfo.version,
description: pluginInfo.description,
author: pluginInfo.author,
dependencies: pluginInfo.dependencies || {},
resources: pluginInfo.resources || [],
link: pluginInfo.link,
repository: {
full_name: repoResponse.data.full_name,
releases: {
total_count: releases.length,
total_downloads: totalDownloads,
releases: releaseStats
}
}
};
} catch (err) {
console.error(`無法取得倉庫訊息: ${err.message}`);
return null;
}
}
async function processPluginFiles(repositoryStats, changedInfoFiles) {
const now = moment().tz('Asia/Taipei').format('YYYY-MM-DD HH:mm:ss');
let oldestIndex = null;
let oldestTime = null;
repositoryStats.forEach((obj, i) => {
const currentTime = obj.updated_at || '';
if (!oldestTime || currentTime < oldestTime) {
oldestTime = currentTime;
oldestIndex = i;
}
});
for (const filePath of changedInfoFiles) {
try {
const pluginInfo = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (pluginInfo.link && pluginInfo.link.includes('github.com') && pluginInfo.name) {
const parts = pluginInfo.link.trim('/').split('/');
if (parts.length >= 2) {
const [owner, repo] = parts.slice(-2);
console.log(`處理倉庫: ${owner}/${repo} (${pluginInfo.name})`);
const stats = await getRepoStats(owner, repo, pluginInfo);
if (stats) {
stats.updated_at = now;
const existingIndex = repositoryStats.findIndex(obj => obj.name === stats.name);
if (existingIndex >= 0) {
repositoryStats[existingIndex] = stats;
console.log(`更新倉庫數據: ${stats.name}`);
} else {
repositoryStats.push(stats);
console.log(`新增倉庫數據: ${stats.name}`);
}
}
}
}
} catch (err) {
console.error(`處理 ${filePath} 時發生錯誤: ${err.message}`);
}
}
if (oldestIndex !== null) {
const oldestRecord = repositoryStats[oldestIndex];
if (oldestRecord.link && oldestRecord.link.includes('github.com')) {
const parts = oldestRecord.link.trim('/').split('/');
if (parts.length >= 2) {
const [owner, repo] = parts.slice(-2);
const stats = await getRepoStats(owner, repo, oldestRecord);
if (stats) {
oldestRecord.repository = stats.repository;
oldestRecord.updated_at = now;
console.log(`更新最舊的倉庫狀態: ${oldestRecord.name}`);
}
}
}
}
return repositoryStats;
}
async function main() {
try {
console.log('初始化資料...');
fs.mkdirpSync('data');
const changedInfoFiles = await getChangedFiles();
if (changedInfoFiles.length === 0) {
console.log('No plugin info files were changed in this PR');
process.exit(0);
}
const statsFile = 'data/repository_stats.json';
let repositoryStats = [];
if (fs.existsSync(statsFile)) {
repositoryStats = JSON.parse(fs.readFileSync(statsFile, 'utf8'));
}
repositoryStats = await processPluginFiles(repositoryStats, changedInfoFiles);
repositoryStats.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''));
fs.writeFileSync(statsFile, JSON.stringify(repositoryStats, null, 0));
console.log('數據更新完成!');
} catch (err) {
console.error(`發生錯誤: ${err.message}`);
process.exit(1);
}
}
main();
EOL
node fetch_stats.js
- name: Commit and push changes
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add data/repository_stats.json
git diff --quiet && git diff --staged --quiet || (git commit -m "Update repository statistics [skip ci]" && git push)