diff --git a/.github/scripts/generate-packages.js b/.github/scripts/generate-packages.js new file mode 100644 index 00000000..07e74603 --- /dev/null +++ b/.github/scripts/generate-packages.js @@ -0,0 +1,170 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Generate packages.json file by scanning the dist directory structure + * @param {string} distPath - Path to the dist directory + * @returns {Object} Generated packages data + */ +function generatePackagesJson(distPath = '.') { + console.log(`Scanning dist directory: ${distPath}`); + + const repositories = []; + + try { + // Read the dist directory + const distDir = fs.readdirSync(distPath, { withFileTypes: true }); + + // Process each subdirectory as a potential repository + for (const dirent of distDir) { + if (dirent.isDirectory() && dirent.name !== '.git') { + console.log(`Processing repository: ${dirent.name}`); + const repoData = scanRepositoryDirectory(path.join(distPath, dirent.name)); + if (repoData) { + repositories.push(repoData); + console.log(` Found ${repoData.releases.length} releases`); + } else { + console.log(` No releases with assets found`); + } + } + } + + // Sort repositories by name + repositories.sort((a, b) => a.name.localeCompare(b.name)); + + // Calculate totals + const totalReleases = repositories.reduce((sum, repo) => sum + repo.releases.length, 0); + const totalAssets = repositories.reduce((sum, repo) => + sum + repo.releases.reduce((releaseSum, release) => releaseSum + release.assetCount, 0), 0 + ); + + const packagesData = { + lastUpdated: new Date().toISOString(), + repositories: repositories, + stats: { + totalRepositories: repositories.length, + totalReleases: totalReleases, + totalAssets: totalAssets + } + }; + + console.log(`Generated packages data:`); + console.log(` Repositories: ${repositories.length}`); + console.log(` Total Releases: ${totalReleases}`); + console.log(` Total Assets: ${totalAssets}`); + + return packagesData; + + } catch (error) { + console.error('Error scanning dist directory:', error); + throw error; + } +} + +/** + * Scan a repository directory to find releases and count assets + * @param {string} repoPath - Path to the repository directory + * @returns {Object|null} Repository data with releases or null if no valid releases + */ +function scanRepositoryDirectory(repoPath) { + const repoName = path.basename(repoPath); + const releases = []; + + try { + if (!fs.existsSync(repoPath) || !fs.statSync(repoPath).isDirectory()) { + return null; + } + + // Scan for release directories + const repoDirContents = fs.readdirSync(repoPath, { withFileTypes: true }); + + for (const dirent of repoDirContents) { + if (dirent.isDirectory()) { + const releaseData = scanReleaseDirectory(path.join(repoPath, dirent.name)); + if (releaseData) { + releases.push(releaseData); + } + } + } + + // Sort releases by tag name (newest first, assuming semantic versioning) + releases.sort((a, b) => b.tag.localeCompare(a.tag, undefined, { numeric: true, sensitivity: 'base' })); + + if (releases.length > 0) { + return { + name: repoName, + releases: releases + }; + } + + return null; + + } catch (error) { + console.error(`Error processing repository ${repoName}:`, error); + return null; + } +} + +/** + * Scan a release directory to count asset files + * @param {string} releasePath - Path to the release directory + * @returns {Object|null} Release data with asset count or null if no assets + */ +function scanReleaseDirectory(releasePath) { + const releaseTag = path.basename(releasePath); + + try { + if (!fs.existsSync(releasePath) || !fs.statSync(releasePath).isDirectory()) { + return null; + } + + // Count actual asset files (exclude hash files and README) + const releaseContents = fs.readdirSync(releasePath, { withFileTypes: true }); + const assetFiles = releaseContents.filter(dirent => { + if (!dirent.isFile()) return false; + + const filename = dirent.name; + // Skip hash files and README + return !(filename.endsWith('.sha256') || + filename.endsWith('.sha512') || + filename.endsWith('.md5') || + filename === 'README.md'); + }); + + if (assetFiles.length > 0) { + return { + tag: releaseTag, + assetCount: assetFiles.length + }; + } + + return null; + + } catch (error) { + console.error(`Error processing release ${releaseTag}:`, error); + return null; + } +} + +/** + * Write packages.json file to the specified path + * @param {Object} packagesData - The packages data to write + * @param {string} outputPath - Path where to write the packages.json file + */ +function writePackagesJson(packagesData, outputPath = './packages.json') { + try { + const jsonString = JSON.stringify(packagesData, null, 2); + fs.writeFileSync(outputPath, jsonString, 'utf8'); + console.log(`Generated packages.json: ${outputPath}`); + } catch (error) { + console.error('Error writing packages.json:', error); + throw error; + } +} + +module.exports = { + generatePackagesJson, + scanRepositoryDirectory, + scanReleaseDirectory, + writePackagesJson +}; diff --git a/.github/workflows/sync-release-assets.yml b/.github/workflows/sync-release-assets.yml index 97f9a383..f2768468 100644 --- a/.github/workflows/sync-release-assets.yml +++ b/.github/workflows/sync-release-assets.yml @@ -67,52 +67,32 @@ jobs: process.chdir('./dist'); await syncReleaseAssets(github, context, isPullRequest, maxNewAssets); - - name: Commit and push changes - if: github.event_name != 'pull_request' - uses: actions-js/push@v1.5 + - name: Generate packages.json + uses: actions/github-script@v8 with: - author_email: ${{ secrets.GH_BOT_EMAIL }} - author_name: ${{ secrets.GH_BOT_NAME }} - branch: dist - directory: dist - github_token: ${{ secrets.GH_BOT_TOKEN }} - message: 'Update release assets - ${{ github.run_id }}' + github-token: ${{ secrets.GH_BOT_TOKEN }} + script: | + // Import the packages generation module + const { generatePackagesJson, writePackagesJson } = require('./.github/scripts/generate-packages.js'); - deploy-website: - needs: sync-assets - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - token: ${{ secrets.GH_BOT_TOKEN }} + console.log('Generating packages.json from dist directory...'); - - name: Create or checkout gh-pages branch into gh-pages directory - run: | - git fetch origin - if git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then - echo "gh-pages branch exists, checking it out" - git worktree add gh-pages origin/gh-pages - cd gh-pages - git pull origin gh-pages - else - echo "gh-pages branch doesn't exist, creating new one" - git worktree add --orphan gh-pages - cd gh-pages - git rm -rf . 2>/dev/null || true - fi + // Change to dist directory + process.chdir('./dist'); - - name: Copy website files to gh-pages directory - run: | - cp -r gh-pages-template/* gh-pages/ + // Generate the packages data + const packagesData = generatePackagesJson('.'); + + // Write the packages.json file + writePackagesJson(packagesData, './packages.json'); - name: Commit and push changes + if: github.event_name != 'pull_request' uses: actions-js/push@v1.5 with: author_email: ${{ secrets.GH_BOT_EMAIL }} author_name: ${{ secrets.GH_BOT_NAME }} - branch: gh-pages - directory: gh-pages + branch: dist + directory: dist github_token: ${{ secrets.GH_BOT_TOKEN }} - message: 'Update from ${{ github.sha }}' + message: 'Update release assets - ${{ github.run_id }}' diff --git a/.github/workflows/update-pages.yml b/.github/workflows/update-pages.yml new file mode 100644 index 00000000..030033ca --- /dev/null +++ b/.github/workflows/update-pages.yml @@ -0,0 +1,49 @@ +--- +name: Build GH-Pages +permissions: + contents: read + +on: + pull_request: + branches: + - master + types: + - opened + - synchronize + - reopened + push: + branches: + - master + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + prep: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: prep + path: gh-pages-template/ + if-no-files-found: error + include-hidden-files: true + retention-days: 1 + + call-jekyll-build: + needs: prep + uses: LizardByte/LizardByte.github.io/.github/workflows/jekyll-build.yml@master + secrets: + GH_BOT_EMAIL: ${{ secrets.GH_BOT_EMAIL }} + GH_BOT_NAME: ${{ secrets.GH_BOT_NAME }} + GH_BOT_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + with: + clean_gh_pages: true + site_artifact: 'prep' + target_branch: 'gh-pages' diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..681f205b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,25 @@ +--- +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + ruby: "3.3" + apt_packages: + - 7zip + - jq + jobs: + install: + - | + mkdir -p "./tmp" + branch="master" + base_url="https://raw.githubusercontent.com/LizardByte/LizardByte.github.io" + url="${base_url}/refs/heads/${branch}/scripts/readthedocs_build.sh" + curl -sSL -o "./tmp/readthedocs_build.sh" "${url}" + chmod +x "./tmp/readthedocs_build.sh" + build: + html: + - "./tmp/readthedocs_build.sh" diff --git a/gh-pages-template/app.js b/gh-pages-template/app.js deleted file mode 100644 index f23e36dc..00000000 --- a/gh-pages-template/app.js +++ /dev/null @@ -1,319 +0,0 @@ -/** - * Repository Data Manager - * Handles loading and managing repository data from GitHub API - */ -class RepositoryDataManager { - constructor() { - this.repositoryData = []; - this.orgName = 'LizardByte'; // Organization name - this.distBranch = 'dist'; - this.apiBase = 'https://api.github.com'; - this.rawBase = 'https://raw.githubusercontent.com'; - } - - /** - * Load repository data by scanning the dist branch via GitHub API - */ - async loadRepositoryData() { - try { - console.log('Loading repository data from GitHub API...'); - - // Get the contents of the dist branch - const response = await fetch(`${this.apiBase}/repos/${this.orgName}/packages/contents?ref=${this.distBranch}`); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status}`); - } - - const contents = await response.json(); - - // Filter for directories (repositories) - const repoDirs = contents.filter(item => item.type === 'dir'); - - console.log(`Found ${repoDirs.length} repository directories`); - - this.repositoryData = []; - - // Process each repository directory - for (const repoDir of repoDirs) { - const repoData = await this.processRepository(repoDir.name); - if (repoData && repoData.releases.length > 0) { - this.repositoryData.push(repoData); - } - } - - console.log(`Loaded data for ${this.repositoryData.length} repositories with releases`); - - return { - repositories: this.repositoryData, - lastUpdated: new Date().toISOString(), - totalRepositories: this.repositoryData.length, - totalReleases: this.repositoryData.reduce((sum, repo) => sum + repo.releases.length, 0), - totalAssets: this.repositoryData.reduce((sum, repo) => - sum + repo.releases.reduce((releaseSum, release) => releaseSum + release.assetCount, 0), 0) - }; - - } catch (error) { - console.error('Error loading repository data:', error); - // Fallback to empty data - this.repositoryData = []; - return null; - } - } - - /** - * Process a single repository directory to get release information - */ - async processRepository(repoName) { - try { - console.log(`Processing repository: ${repoName}`); - - // Get repository directory contents - const response = await fetch(`${this.apiBase}/repos/${this.orgName}/packages/contents/${repoName}?ref=${this.distBranch}`); - - if (!response.ok) { - console.warn(`Could not fetch contents for ${repoName}: ${response.status}`); - return null; - } - - const contents = await response.json(); - - // Filter for directories (releases) - const releaseDirs = contents.filter(item => item.type === 'dir'); - - if (releaseDirs.length === 0) { - console.log(`No release directories found for ${repoName}`); - return null; - } - - const repoData = { - name: repoName, - releases: [] - }; - - // Process each release directory - for (const releaseDir of releaseDirs) { - const releaseData = await this.processRelease(repoName, releaseDir.name); - if (releaseData) { - repoData.releases.push(releaseData); - } - } - - // Sort releases by tag name (newest first, assuming semantic versioning) - repoData.releases.sort((a, b) => b.tag.localeCompare(a.tag, undefined, { numeric: true, sensitivity: 'base' })); - - return repoData; - - } catch (error) { - console.error(`Error processing repository ${repoName}:`, error); - return null; - } - } - - /** - * Process a single release directory to count assets - */ - async processRelease(repoName, releaseTag) { - try { - // Get release directory contents - const response = await fetch(`${this.apiBase}/repos/${this.orgName}/packages/contents/${repoName}/${releaseTag}?ref=${this.distBranch}`); - - if (!response.ok) { - console.warn(`Could not fetch release contents for ${repoName}/${releaseTag}: ${response.status}`); - return null; - } - - const contents = await response.json(); - - // Count actual asset files (exclude hash files) - const assetFiles = contents.filter(item => - item.type === 'file' && - !item.name.endsWith('.sha256') && - !item.name.endsWith('.sha512') && - !item.name.endsWith('.md5') && - item.name !== 'README.md' - ); - - if (assetFiles.length === 0) { - return null; - } - - return { - tag: releaseTag, - assetCount: assetFiles.length - }; - - } catch (error) { - console.error(`Error processing release ${repoName}/${releaseTag}:`, error); - return null; - } - } - - /** - * Get all repository data - */ - getRepositories() { - return this.repositoryData; - } - - /** - * Filter repositories based on search term - */ - filterRepositories(searchTerm) { - if (!searchTerm) { - return this.repositoryData; - } - - return this.repositoryData.filter(repo => { - const repoMatch = repo.name.toLowerCase().includes(searchTerm.toLowerCase()); - const releaseMatch = repo.releases.some(release => - release.tag.toLowerCase().includes(searchTerm.toLowerCase())); - return repoMatch || releaseMatch; - }); - } -} - -/** - * UI Manager - * Handles all DOM manipulation and rendering - */ -class UIManager { - constructor() { - this.repositoryGrid = document.getElementById('repositoryGrid'); - this.searchInput = document.getElementById('searchInput'); - this.repoCountElement = document.getElementById('repoCount'); - this.releaseCountElement = document.getElementById('releaseCount'); - this.assetCountElement = document.getElementById('assetCount'); - this.updateTimeElement = document.getElementById('updateTime'); - this.orgName = 'LizardByte'; - } - - /** - * Render repositories in the grid - */ - renderRepositories(repos) { - if (repos.length === 0) { - this.repositoryGrid.innerHTML = '
Centralized storage for all release assets from LizardByte repositories
-Repositories
+Releases
+Assets
+