@sdeverywhere/plugin-deploy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts","../src/copy-products.ts","../src/store-artifacts.ts","../src/validate-branch-name.ts"],"sourcesContent":["// Copyright (c) 2025 Climate Interactive / New Venture Fund\n\nimport { existsSync, mkdirSync, rmSync } from 'node:fs'\nimport { isAbsolute, join as joinPath } from 'node:path'\n\nimport type { BuildContext, Plugin } from '@sdeverywhere/build'\n\nimport { copyProducts } from './copy-products'\nimport type { BuildProduct, DeployPluginOptions, ResolvedPluginOptions } from './options'\nimport { storeArtifacts } from './store-artifacts'\nimport { validateBranchName } from './validate-branch-name'\n\nexport function deployPlugin(options?: DeployPluginOptions): Plugin {\n return new DeployPlugin(options ?? {})\n}\n\nclass DeployPlugin implements Plugin {\n constructor(private readonly userOptions: DeployPluginOptions) {}\n\n async init(): Promise<void> {\n // Validate branch name and fail the build if it is invalid\n const branchName = getCurrentBranchName()\n if (branchName) {\n validateBranchName(branchName)\n }\n }\n\n async postBuild(context: BuildContext): Promise<boolean> {\n if (context.config.mode !== 'production') {\n // No deployment in dev mode\n return true\n }\n\n context.log('info', '\\nPreparing to deploy build products...')\n\n // Resolve the plugin options\n const resolvedOptions = resolveOptions(context, this.userOptions)\n\n // Remove existing deploy directory if it exists\n const deployDir = resolvedOptions.deployDir\n if (existsSync(deployDir)) {\n context.log('verbose', 'Removing existing deploy directory...')\n rmSync(deployDir, { recursive: true, force: true })\n }\n\n // Create deploy directory\n context.log('verbose', 'Creating deploy directory...')\n mkdirSync(deployDir, { recursive: true })\n\n // Copy build products to the deploy directory\n context.log('verbose', 'Copying build products to deploy directory...')\n copyProducts(context, resolvedOptions)\n\n // XXX: Skip the `storeArtifacts` step if running tests\n if (process.env.VITEST === 'true') {\n context.log('info', 'Skipping `storeArtifacts` step in test mode...')\n return true\n }\n\n // Get the name of the current branch. If `GITHUB_REF_NAME` is not defined,\n // skip storing artifacts.\n const currentBranchName = getCurrentBranchName()\n if (!currentBranchName) {\n // TODO: For now we will only proceed with the `storeArtifacts` step if `GITHUB_REF_NAME` is defined\n // so that the plugin can be tested locally (up through the `copyProducts` step). We should make\n // this behavior configurable in the options.\n context.log(\n 'info',\n 'Failed to get the name of the current branch (GITHUB_REF_NAME is not defined); artifacts branch will not be updated'\n )\n return true\n }\n\n // Update the artifacts branch with the build products\n context.log('verbose', 'Updating artifacts branch with build products...')\n return storeArtifacts(context, resolvedOptions, currentBranchName)\n }\n}\n\nfunction getRepoOwnerAndName(): [string, string] | undefined {\n const ownerAndRepo = process.env.GITHUB_REPOSITORY\n if (ownerAndRepo) {\n const [owner, repo] = ownerAndRepo.split('/')\n return [owner, repo]\n } else {\n return undefined\n }\n}\n\nfunction getCurrentBranchName(): string | undefined {\n if (process.env.VITEST) {\n return process.env.TEST_BRANCH_NAME\n } else {\n return process.env.GITHUB_REF_NAME\n }\n}\n\nfunction resolveOptions(context: BuildContext, userOptions: DeployPluginOptions): ResolvedPluginOptions {\n // Resolve the base URL\n let baseUrl: string\n if (userOptions.baseUrl) {\n // Use the provided base URL\n baseUrl = userOptions.baseUrl\n } else {\n // Determine the default base URL from the repository owner and name\n // TODO: This assumes GitHub for now, but we should support other hosts\n const repoOwnerAndName = getRepoOwnerAndName()\n if (repoOwnerAndName) {\n const [owner, repo] = repoOwnerAndName\n baseUrl = `https://${owner}.github.io/${repo}`\n } else {\n context.log(\n 'info',\n 'Failed to get the name of the repository (GITHUB_REPOSITORY is not defined); the deploy directory will be populated, but the artifacts branch will not be updated'\n )\n baseUrl = undefined\n }\n }\n\n // Resolve the absolute path to the deploy directory\n let deployDir: string\n if (userOptions.deployDir) {\n if (isAbsolute(userOptions.deployDir)) {\n deployDir = userOptions.deployDir\n } else {\n deployDir = joinPath(context.config.rootDir, userOptions.deployDir)\n }\n } else {\n deployDir = joinPath(context.config.prepDir, 'deploy')\n }\n\n // Resolve the build products from the options, or if undefined, use default set\n let products: Record<string, BuildProduct>\n let defaultProducts: boolean\n if (userOptions.products) {\n products = userOptions.products\n defaultProducts = false\n } else {\n products = {\n app: {\n displayName: 'app',\n srcPath: 'packages/app/public',\n dstPath: 'app'\n },\n checkReport: {\n displayName: 'checks',\n srcPath: 'sde-prep/check-report',\n dstPath: 'extras/check-compare-to-base'\n },\n checkBundle: {\n srcPath: 'sde-prep/check-bundle.js',\n dstPath: 'extras/check-bundle.js'\n }\n }\n defaultProducts = true\n }\n\n return {\n baseUrl,\n deployDir,\n products,\n defaultProducts\n }\n}\n","// Copyright (c) 2025 Climate Interactive / New Venture Fund\n\nimport { cpSync, existsSync, mkdirSync } from 'node:fs'\nimport { isAbsolute, join as joinPath } from 'node:path'\n\nimport type { BuildContext } from '@sdeverywhere/build'\n\nimport type { ResolvedPluginOptions } from './options'\n\n/**\n * Copy the build products to the deployment directory.\n *\n * @param context The build context.\n * @param options The resolved plugin options.\n */\nexport function copyProducts(context: BuildContext, options: ResolvedPluginOptions): void {\n // Helper function to copy a file or directory to the deployment directory\n function copyToDeployDir(src: string, dst: string): void {\n // Resolve the path to the source file or directory\n let fullSrcPath: string\n if (isAbsolute(src)) {\n fullSrcPath = src\n } else {\n fullSrcPath = joinPath(context.config.rootDir, src)\n }\n\n // Skip this product if we are copying the default products and the source file/dir doesn't exist\n const skipIfNotExists = options.defaultProducts\n if (skipIfNotExists && !existsSync(fullSrcPath)) {\n return\n }\n\n context.log('verbose', `Copying '${src}' to '${dst}'...`)\n\n // Resolve the path to the destination file or directory\n const fullDstPath = joinPath(options.deployDir, dst)\n\n // Create the destination directories, if needed\n if (existsSync(fullDstPath)) {\n mkdirSync(fullDstPath, { recursive: true })\n }\n\n // Copy the file or directory to the destination\n cpSync(fullSrcPath, fullDstPath, { recursive: true })\n }\n\n // Copy each build product to the deployment directory\n for (const product of Object.values(options.products)) {\n copyToDeployDir(product.srcPath, product.dstPath)\n }\n}\n","// Copyright (c) 2025 Climate Interactive / New Venture Fund\n\nimport { execSync } from 'node:child_process'\nimport { existsSync, mkdirSync, cpSync, readFileSync, rmSync, writeFileSync } from 'node:fs'\nimport { join as joinPath } from 'node:path'\n\nimport type { BuildContext } from '@sdeverywhere/build'\n\nimport type { ResolvedPluginOptions } from './options'\n\n// TODO: Make these configurable\nconst artifactsBranchName = 'artifacts'\nconst artifactsDir = 'artifacts'\n\n/**\n * Simplified version of `BuildProduct` for use in the JSON metadata file.\n */\ninterface ProductSpec {\n /**\n * The name of the build product as used in the link in the top-level index page. If undefined,\n * no link will be included in the top-level index page.\n */\n displayName?: string\n\n /** The path of the build product, relative to the base URL. */\n path: string\n}\n\n/**\n * Describes a branch build in the `artifacts` directory. This is the shape of\n * the records in the `metadata/index.json` file.\n *\n * Example:\n * ```json\n * {\n * \"name\": \"main\",\n * \"path\": \"branch/main\",\n * \"lastModified\": \"2025-01-01T00:00:00Z\",\n * \"products\": {\n * \"app\": {\n * \"displayName\": \"app\",\n * \"path\": \"branch/main/app\"\n * },\n * \"checkReport\": {\n * \"displayName\": \"checks\",\n * \"path\": \"branch/main/extras/check-compare-to-base\"\n * },\n * \"checkBundle\": {\n * \"path\": \"branch/main/extras/check-bundle.js\"\n * }\n * }\n * }\n * ```\n */\ninterface BranchSpec {\n /** The name of the branch. */\n name: string\n /** The path of the branch build directory, relative to the base URL. */\n path: string\n /** The last modified date of the branch build products in ISO 8601 format. */\n lastModified: string\n /**\n * The build products that are available for this branch. The keys of the record should be a short\n * name for the build product, used in the `index.json` file that is generated by the plugin.\n */\n products: Record<string, ProductSpec>\n}\n\n/**\n * Store build artifacts in the `artifacts` orphan branch.\n *\n * The `artifacts` directory contains build artifacts for all branches that\n * have been built. The directory structure is as follows:\n * ```\n * artifacts/\n * ├── index.html # Top-level index.html file\n * ├── latest/\n * | ├── index.html # Main branch app\n * | ├── assets/ # Main branch assets\n * ├── branch/\n * | ├── main/\n * | │ ├── app/ # Main branch app files\n * | │ └── extras/ # Main branch check bundles and reports\n * | ├── chris/1234-test/\n * | │ ├── app/ # Feature branch app files\n * | │ └── extras/ # Feature branch check bundles and reports\n * | └── feature/new-ui/\n * | ├── app/ # Feature branch app files\n * | └── extras/ # Feature branch check bundles and reports\n * └── metadata/\n * ├── bundles.json # Listing of available bundles\n * └── index.json # Listing of available branch builds\n * ```\n *\n * @param context The build context.\n * @param options The resolved plugin options.\n * @param currentBranchName The current (sanitized/validated) git branch name.\n */\nexport function storeArtifacts(\n context: BuildContext,\n options: ResolvedPluginOptions,\n currentBranchName: string\n): boolean {\n const deployDir = options.deployDir\n if (!existsSync(deployDir)) {\n context.log('error', `ERROR: Deployment directory '${deployDir}' does not exist`)\n return false\n }\n\n try {\n // Check if the project is already set up for GitHub Pages\n if (!checkGitHubPagesConfiguration(context)) {\n return false\n }\n\n context.log('info', `Storing artifacts for branch '${currentBranchName}'...`)\n\n // Configure git so that the commits in the following steps are attributed to\n // the current user\n const githubActor = process.env.GITHUB_ACTOR\n if (!githubActor) {\n context.log(\n 'error',\n 'Failed to get the GitHub username associated with the latest push (GITHUB_ACTOR is not defined); the artifacts branch will not be updated'\n )\n return false\n }\n execSync(`git config user.name \"${githubActor}\"`, { stdio: 'inherit' })\n execSync(`git config user.email \"${githubActor}@users.noreply.github.com\"`, { stdio: 'inherit' })\n\n // Check if `artifacts` branch exists\n let artifactsExists\n try {\n // This will fail if the branch doesn't exist yet, which is what we want. This\n // assumes that the checkout action step in the `build` workflow was configured\n // with `fetch-depth: 0` to fetch full history.\n const branchRef = `refs/remotes/origin/${artifactsBranchName}`\n execSync(`git show-ref --verify --quiet ${branchRef}`, { stdio: 'ignore' })\n artifactsExists = true\n } catch (_) {\n artifactsExists = false\n }\n\n if (!artifactsExists) {\n // The orphan `artifacts` branch doesn't already exist, so create it now\n context.log('verbose', `Creating orphan '${artifactsBranchName}' branch...`)\n execSync(`git checkout --orphan ${artifactsBranchName}`, { stdio: 'inherit' })\n\n // By default, the new branch will inherit the contents of the current branch\n // (i.e., the contents will be git added), but we only want to keep the `artifacts`\n // directory, so unstage all cached files first\n execSync('git rm -rf --cached .', { stdio: 'inherit' })\n\n // Add a `.gitignore` file that ignores everything except the `artifacts` directory\n // and the `.gitignore` file itself\n const ignoredFiles = ['*', '!artifacts', '!artifacts/**', '!.gitignore']\n writeFileSync('.gitignore', ignoredFiles.join('\\n'))\n execSync('git add .gitignore', { stdio: 'inherit' })\n context.log('verbose', `Created .gitignore file for '${artifactsBranchName}' branch`)\n } else {\n // Switch to the existing `artifacts` branch\n context.log('verbose', `Switching to existing '${artifactsBranchName}' branch...`)\n execSync(`git checkout ${artifactsBranchName}`, { stdio: 'inherit' })\n }\n\n // Remove existing branch directory if it exists\n const currentBranchDir = joinPath(artifactsDir, 'branch', currentBranchName)\n if (existsSync(currentBranchDir)) {\n context.log('verbose', `Removing existing branch directory '${currentBranchDir}'...`)\n rmSync(currentBranchDir, { recursive: true })\n }\n\n // Copy files from deploy directory to branch directory\n context.log('verbose', `Copying staged files from '${deployDir}' to '${currentBranchDir}'...`)\n cpSync(deployDir, currentBranchDir, { recursive: true })\n\n // Update `metadata/index.json` to include the current branch\n const currentBranchUrlPath = `branch/${currentBranchName}`\n const productSpecs: Record<string, ProductSpec> = {}\n for (const [name, product] of Object.entries(options.products || {})) {\n productSpecs[name] = {\n displayName: product.displayName,\n path: `${currentBranchUrlPath}/${product.dstPath}`\n }\n }\n const currentBranchSpec: BranchSpec = {\n name: currentBranchName,\n path: currentBranchUrlPath,\n lastModified: new Date().toISOString(),\n products: productSpecs\n }\n updateMetadata(context, options.baseUrl, currentBranchSpec)\n\n // For main branch, also copy app files to top-level `latest` directory\n // TODO: Make this step optional; this assumes we are deploying to a single server\n // that includes both production and development builds, but it would be better to\n // support separate servers for production and development builds\n // TODO: Make the main branch name configurable\n if (currentBranchName === 'main') {\n const stagedAppSrcDir = joinPath(deployDir, 'app')\n if (existsSync(stagedAppSrcDir)) {\n context.log('verbose', `Copying main branch app files to 'latest' directory...`)\n if (existsSync('latest')) {\n context.log('verbose', `Removing existing 'latest' directory...`)\n rmSync('latest', { recursive: true })\n }\n const stagedAppDstDir = joinPath(artifactsDir, 'latest')\n cpSync(stagedAppSrcDir, stagedAppDstDir, { recursive: true })\n }\n }\n\n // Squash commits so that we only keep the most recent 5 commits, but still preserve\n // the full contents of the `artifacts` branch. This prevents the artifacts branch\n // from growing too large. Note that we keep the latest build for each feature branch\n // indefinitely, so the the `artifacts` may eventually grow larger than desired. In\n // this case, the user can manually remove old branch build artifacts to further\n // reduce the size of the `artifacts` branch.\n try {\n const commitCount = parseInt(execSync('git rev-list --count HEAD', { encoding: 'utf8' }).trim())\n if (commitCount > 5) {\n context.log(\n 'verbose',\n `Found ${commitCount} commits, squashing older commits to reduce size of '${artifactsBranchName}' branch...`\n )\n // Get the hash of the 5th most recent commit (0-indexed, so skip 4)\n const fifthCommitHash = execSync('git rev-list --skip=4 --max-count=1 HEAD', { encoding: 'utf8' }).trim()\n // Reset soft to that commit (keeps all changes staged)\n execSync(`git reset --soft ${fifthCommitHash}`, { stdio: 'inherit' })\n // Amend the commit to squash everything into one\n execSync('git commit --amend --no-edit', { stdio: 'inherit' })\n context.log('verbose', 'Successfully squashed older commits')\n }\n } catch (_) {\n context.log('verbose', 'No commits found or error checking commit history, continuing...')\n }\n\n // Add all updated files in the `artifacts` directory to git\n execSync(`git add ${artifactsDir}`, { stdio: 'inherit' })\n\n // Check if there are changes to commit\n try {\n execSync('git diff --cached --quiet', { stdio: 'ignore' })\n context.log('verbose', 'No changes to commit')\n } catch (_) {\n // There are changes, so commit them\n const commitMessage = `build: update artifacts for branch ${currentBranchName}`\n execSync(`git commit -m \"${commitMessage}\"`, { stdio: 'inherit' })\n context.log('verbose', `Committed artifacts for branch '${currentBranchName}'...`)\n }\n\n // Push to remote (we do a force push since we may have rewritten history)\n context.log('info', `Pushing '${artifactsBranchName}' branch to remote...`)\n execSync(`git push --force origin ${artifactsBranchName}`, { stdio: 'inherit' })\n\n context.log('info', `✅ Successfully stored artifacts for branch '${currentBranchName}'`)\n return true\n } catch (error) {\n // TODO: Use `logError` here once it is available in `BuildContext` and pass error\n context.log('error', '❌ Error storing artifacts:')\n console.error(error)\n return false\n } finally {\n // Switch back to the original branch\n context.log('verbose', `Switching to original '${currentBranchName}' branch...`)\n execSync(`git checkout ${currentBranchName}`, { stdio: 'inherit' })\n }\n}\n\n/**\n * Check if GitHub Pages is configured for this repo.\n *\n * @param context The build context.\n * @returns true if GitHub Pages is configured, false otherwise.\n */\nfunction checkGitHubPagesConfiguration(context: BuildContext): boolean {\n context.log('verbose', 'Checking if GitHub Pages is configured for this repo...')\n const ownerAndRepo = process.env.GITHUB_REPOSITORY\n const ghAccept = '\"Accept: application/vnd.github+json\"'\n const ghApiVersion = '\"X-GitHub-Api-Version: 2022-11-28\"'\n const ghPagesApiPath = `/repos/${ownerAndRepo}/pages`\n let isGitHubPagesSetup\n try {\n const pagesResponse = execSync(`gh api -H ${ghAccept} -H ${ghApiVersion} ${ghPagesApiPath}`)\n const pagesMetadata = JSON.parse(pagesResponse.toString())\n isGitHubPagesSetup = pagesMetadata.build_type === 'workflow'\n if (!isGitHubPagesSetup) {\n context.log(\n 'verbose',\n 'GitHub Pages is not configured to use workflow builds for this repo, but found existing configuration:'\n )\n context.log('verbose', ' build_type: ' + pagesMetadata.build_type)\n context.log('verbose', ' source.branch: ' + pagesMetadata.source?.branch)\n context.log('verbose', ' source.path: ' + pagesMetadata.source?.path)\n }\n } catch (_) {\n context.log('verbose', 'No existing GitHub Pages configuration found')\n isGitHubPagesSetup = false\n }\n if (isGitHubPagesSetup) {\n context.log('verbose', 'GitHub Pages is already configured for this repo')\n return true\n } else {\n // XXX: Ideally we would set up GitHub Pages automatically using the GitHub API,\n // but that requires \"administration\" permissions, which are not available for\n // the standard GITHUB_TOKEN. Using a PAT would require even more effort to set\n // up, so for now, show instructions and fail the build.\n const msg: string[] = []\n msg.push('GitHub Pages is not configured for this repo')\n msg.push('For now, you must manually enable GitHub Pages as follows:')\n msg.push(' 1. Go to the GitHub repository settings')\n msg.push(' 2. In the sidebar, select \"Pages\"')\n msg.push(' 3. Under \"Build and deployment\", change \"Source\" to \"GitHub Actions\"')\n msg.push(' 4. In the tab bar, select \"Actions\"')\n msg.push(' 5. Click on the most recent failed workflow run')\n msg.push(' 6. In the upper right corner, click \"Re-run jobs\" then \"Re-run all jobs\"')\n context.log('error', msg.join('\\n'))\n return false\n }\n}\n\n/**\n * Update the `metadata/index.json` file with the URL paths for the branch and its artifacts,\n * update the `metadata/bundles.json` file with the available bundles, and generate a top-level\n * `index.html` file that lists all available branch builds.\n */\nfunction updateMetadata(context: BuildContext, baseUrl: string, currentBranchSpec: BranchSpec) {\n const metadataDir = joinPath(artifactsDir, 'metadata')\n const indexJsonFile = joinPath(metadataDir, 'index.json')\n const bundlesJsonFile = joinPath(metadataDir, 'bundles.json')\n\n // Create metadata directory if it doesn't exist\n mkdirSync(metadataDir, { recursive: true })\n\n // Read the existing `index.json` file if it exists\n let branchSpecs: BranchSpec[] = []\n if (existsSync(indexJsonFile)) {\n try {\n branchSpecs = JSON.parse(readFileSync(indexJsonFile, 'utf8'))\n } catch (_) {\n context.log('info', '⚠️ Could not parse existing index.json file, starting fresh')\n branchSpecs = []\n }\n }\n\n // Remove existing spec for this branch\n branchSpecs = branchSpecs.filter(spec => spec.name !== currentBranchSpec.name)\n\n // Add the new branch spec\n branchSpecs.push(currentBranchSpec)\n\n // Sort by lastModified (newest first)\n branchSpecs.sort((a, b) => b.lastModified.localeCompare(a.lastModified))\n\n // Write updated branch metadata to `metadata/index.json`\n writeFileSync(indexJsonFile, JSON.stringify(branchSpecs, null, 2))\n\n // Derive a `bundles.json` file from the branch specs. The `bundles.json` file\n // must be in a specific format expected by that the model-check tool\n interface BundleSpec {\n name: string\n url: string\n lastModified: string\n }\n const bundleSpecs: BundleSpec[] = []\n for (const branchSpec of branchSpecs) {\n // TODO: For now we check for a specific build product with key `checkBundle`; we should\n // make this configurable instead of expecting a specific key format\n if (branchSpec.products?.checkBundle) {\n bundleSpecs.push({\n name: branchSpec.name,\n // TODO: For now we store the full URL to the bundle in the `bundles.json` file using\n // the `baseUrl` value from the plugin options. Ideally we could use a relative URL\n // here instead of encoding the full URL in the `bundles.json` file, but\n // `@sdeverywhere/plugin-check` currently doesn't understand relative URLs.\n url: `${baseUrl}/${branchSpec.products.checkBundle.path}`,\n lastModified: branchSpec.lastModified\n })\n }\n }\n\n // Write updated bundle metadata to `metadata/bundles.json`\n writeFileSync(bundlesJsonFile, JSON.stringify(bundleSpecs, null, 2))\n\n context.log('verbose', `Updated metadata for branch '${currentBranchSpec.name}'`)\n\n // Generate top-level `index.html` file\n generateIndexHtml(context, branchSpecs)\n}\n\n/**\n * Generate a top-level index.html file that lists all available branch builds.\n */\nfunction generateIndexHtml(context: BuildContext, branchSpecs: BranchSpec[]) {\n const indexHtmlPath = joinPath(artifactsDir, 'index.html')\n\n const latestBuildDate = branchSpecs.find(branch => branch.name === 'main')?.lastModified || ''\n\n function getBranchLinksHtml(branch: BranchSpec): string {\n const links: string[] = []\n for (const product of Object.values(branch.products || {})) {\n // Only include a link if the product has a display name\n if (product.displayName) {\n links.push(`<a href=\"${product.path}\">${product.displayName}</a>`)\n }\n }\n return links.join('<span class=\"separator\">|</span>')\n }\n\n const htmlContent = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Branch Builds</title>\n <style>\n body {\n font-family: monospace;\n margin: 0;\n padding: 20px;\n background-color: #f5f5f5;\n color: #000;\n }\n a, a:visited {\n color: #2563eb;\n }\n hr {\n margin: 20px 0;\n border: none;\n border-top: 1px solid #ccc;\n }\n .container {\n overflow: hidden;\n }\n .grid {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 0 20px;\n }\n .header {\n padding: 8px 0;\n font-weight: 600;\n }\n .row {\n display: contents;\n }\n .cell {\n display: flex;\n flex-direction: column;\n padding: 8px 0;\n }\n .links {\n display: flex;\n }\n .separator {\n margin: 0 4px;\n color: #9ca3af;\n }\n </style>\n <script>\n function formatDate(isoString) {\n if (isoString.length === 0) {\n return 'n/a'\n }\n const date = new Date(isoString)\n const dateString = date.toLocaleDateString(undefined, { day: 'numeric', month: 'numeric', year: 'numeric' })\n const timeString = date.toLocaleTimeString(undefined, { hour12: false, hour: '2-digit', minute: '2-digit' })\n return \\`\\${dateString} at \\${timeString}\\`\n }\n\n // Format all dates on page load so that the time is displayed in the user's local timezone\n document.addEventListener('DOMContentLoaded', function() {\n document.querySelectorAll('.date').forEach(span => {\n const timestamp = span.getAttribute('data-timestamp')\n if (timestamp) {\n span.textContent = formatDate(timestamp)\n }\n })\n })\n </script>\n</head>\n<body>\n <div class=\"container\">\n <a href=\"latest\">Latest production app</a>\n <br />\n (last updated: <span class=\"date\" data-timestamp=\"${latestBuildDate}\"></span>)\n <hr />\n <div class=\"grid\">\n <div class=\"header\">Branch</div>\n <div class=\"header\">Last Updated</div>\n${branchSpecs\n .map(\n branch => `\n <div class=\"row\">\n <div class=\"cell\">\n <span class=\"branch-name\">${branch.name}</span>\n <div class=\"links\">\n ${getBranchLinksHtml(branch)}\n </div>\n </div>\n <div class=\"cell\">\n <span class=\"date\" data-timestamp=\"${branch.lastModified}\"></span>\n </div>\n </div>`\n )\n .join('')}\n </div>\n </div>\n</body>\n</html>`\n\n // Write the HTML content to the `index.html` file\n writeFileSync(indexHtmlPath, htmlContent)\n context.log('verbose', `Generated top-level index.html`)\n}\n","// Copyright (c) 2025 Climate Interactive / New Venture Fund\n\n/**\n * Validate the given branch name to ensure it meets requirements for URL-safe paths.\n *\n * Allows: /, -, _, a-z, A-Z, 0-9\n *\n * @param branchName The branch name to check.\n * @throws Error if the branch name is invalid.\n */\nexport function validateBranchName(branchName: string): void {\n // Allow: /, -, _, a-z, A-Z, 0-9\n const validBranchPattern = /^[/\\-_a-zA-Z0-9]+$/\n if (!validBranchPattern.test(branchName)) {\n throw new Error(\n `Branch name '${branchName}' contains invalid characters; branch names must only contain: /, -, _, a-z, A-Z, 0-9`\n )\n }\n\n // Additional validation: cannot start or end with /\n if (branchName.startsWith('/') || branchName.endsWith('/')) {\n throw new Error(`Branch name '${branchName}' cannot start or end with \"/\"`)\n }\n\n // Additional validation: cannot have consecutive slashes\n if (branchName.includes('//')) {\n throw new Error(`Branch name '${branchName}' cannot contain consecutive slashes \"//\"`)\n }\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","rmSync","isAbsolute","joinPath","existsSync","mkdirSync","cpSync","joinPath","existsSync","rmSync","mkdirSync","isAbsolute","joinPath"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@sdeverywhere/plugin-deploy",
3
+ "version": "0.1.0",
4
+ "files": [
5
+ "bin/**",
6
+ "dist/**",
7
+ "template-bundle/**",
8
+ "template-report/**",
9
+ "template-tests/**"
10
+ ],
11
+ "type": "module",
12
+ "main": "dist/index.cjs",
13
+ "module": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "peerDependencies": {
23
+ "@sdeverywhere/build": "^0.3.7"
24
+ },
25
+ "devDependencies": {
26
+ "@sdeverywhere/build": "*",
27
+ "@types/node": "^20.14.8"
28
+ },
29
+ "author": "Climate Interactive",
30
+ "license": "MIT",
31
+ "homepage": "https://sdeverywhere.org",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/climateinteractive/SDEverywhere.git",
35
+ "directory": "packages/plugin-deploy"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/climateinteractive/SDEverywhere/issues"
39
+ },
40
+ "scripts": {
41
+ "clean": "rm -rf dist",
42
+ "lint": "eslint src --max-warnings 0",
43
+ "prettier:check": "prettier --check .",
44
+ "prettier:fix": "prettier --write .",
45
+ "precommit": "../../scripts/precommit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "test:ci": "vitest run",
49
+ "type-check": "tsc --noEmit -p tsconfig-test.json",
50
+ "build": "tsup",
51
+ "docs": "../../scripts/gen-docs.js",
52
+ "ci:build": "run-s clean lint prettier:check test:ci type-check build docs"
53
+ }
54
+ }