diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d51ead3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: weekly + multi-ecosystem-groups: + "all-deps": + patterns: + - "*" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: monthly + +groups: + "frontend-backend": + patterns: + - "@myorg/frontend*" + - "@myorg/backend*" diff --git a/.gitignore b/.gitignore index 9308a4b..2e9a039 100644 --- a/.gitignore +++ b/.gitignore @@ -198,6 +198,10 @@ _minted* # nomencl *.nlg *.nlo + +# Node dependencies +/node_modules + *.nls # pax diff --git a/README.md b/README.md index 1de0032..a5ecd1e 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,23 @@ TODO: Complete applicable items based on your project type --- +## 🧰 Automation & CI + +This repository includes a helper script to validate the +[Dependabot](https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically) configuration +and ensure **groups or multi-ecosystem groups** are defined per issue +[#73](https://github.com/AOSSIE-Org/Template-Repo/issues/73). + +Run the validator with Node: + +```bash +node scripts/validate-dependabot.js +``` + +Tests for the validator are located under `tests/dependabot.test.js` +and executed via `npm test` (sample configuration is in +`.github/dependabot.yml`). + ## 🔗 Repository Links TODO: Update with your repository structure diff --git a/package.json b/package.json new file mode 100644 index 0000000..899348c --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "template-repo", + "version": "0.1.0", + "private": true, + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "check": "echo \"No checks configured yet\"" + }, + "devDependencies": { + "vitest": "^1.0.0", + "js-yaml": "^4.1.0" + } +} diff --git a/scripts/validate-dependabot.js b/scripts/validate-dependabot.js new file mode 100644 index 0000000..745e45e --- /dev/null +++ b/scripts/validate-dependabot.js @@ -0,0 +1,61 @@ +const fs = require('fs'); +const yaml = require('js-yaml'); + +/** + * Load dependabot configuration and check whether it contains + * either (1) top-level "groups" definitions, or (2) multi-ecosystem groups + * under "updates" entries. + * + * Returns an object {ok: boolean, issues: string[]} where ok is true if + * configuration satisfies the feature requirement (groups or multi-ecosystem groups). + */ +function validateDependabot(path = '.github/dependabot.yml') { + if (!fs.existsSync(path)) { + return { ok: false, issues: [`file not found: ${path}`] }; + } + const raw = fs.readFileSync(path, 'utf-8'); + let doc; + try { + doc = yaml.load(raw); + } catch (e) { + return { ok: false, issues: ['YAML parse error: ' + e.message] }; + } + + const issues = []; + // if the document isn't an object, treat as parse error + if (typeof doc !== 'object' || doc === null) { + return { ok: false, issues: ['YAML parse error: document is not an object'] }; + } + + // the structure: typically at root there's "updates" array. + if (!doc || !Array.isArray(doc.updates)) { + issues.push('missing updates array'); + return { ok: false, issues }; + } + + // check if any update has groups or if there's top-level groups + let foundGroup = false; + if (doc.groups && Object.keys(doc.groups).length > 0) { + foundGroup = true; + } + + for (const upd of doc.updates) { + if (upd.groups && Object.keys(upd.groups).length > 0) { + foundGroup = true; + } + if (upd['multi-ecosystem'] || upd['multi-ecosystem-groups']) { + foundGroup = true; + } + } + + if (!foundGroup) { + issues.push('no groups or multi-ecosystem groups defined'); + } + + return { ok: foundGroup, issues }; +} + +// Export for tests +module.exports = { + validateDependabot, +}; diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000..893eb4e --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +Write-Host "Running setup (PowerShell)" +if (!(Test-Path package.json)) { + Write-Error "package.json not found. Aborting." + exit 1 +} +Write-Host "Installing npm dependencies..." +npm install +if ($LASTEXITCODE -ne 0) { Write-Error "npm install failed"; exit $LASTEXITCODE } +Write-Host "Running tests..." +npm test +exit $LASTEXITCODE diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..ea1aa94 --- /dev/null +++ b/setup.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail +echo "Running setup (bash)" +if [ ! -f package.json ]; then + echo "package.json not found. Aborting." >&2 + exit 1 +fi +echo "Installing npm dependencies..." +npm install +echo "Running tests..." +npm test diff --git a/tests/dependabot.test.js b/tests/dependabot.test.js new file mode 100644 index 0000000..96c4a08 --- /dev/null +++ b/tests/dependabot.test.js @@ -0,0 +1,104 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import { validateDependabot } from '../scripts/validate-dependabot.js'; + +// Helper to write temporary config +function writeConfig(content) { + const file = path.join(process.cwd(), '.github', 'dependabot.yml'); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content); +} + +function removeConfig() { + const file = path.join(process.cwd(), '.github', 'dependabot.yml'); + if (fs.existsSync(file)) fs.unlinkSync(file); +} + +describe('dependabot configuration validation', () => { + afterAll(() => { + removeConfig(); + }); + + it('fails when file is missing', () => { + removeConfig(); + const result = validateDependabot(); + expect(result.ok).toBe(false); + expect(result.issues).toContain('file not found: .github/dependabot.yml'); + }); + + it('fails with malformed yaml', () => { + writeConfig('::notyaml'); + const result = validateDependabot(); + expect(result.ok).toBe(false); + expect(result.issues[0]).toMatch(/YAML parse error/); + }); + + it('fails when no updates array', () => { + writeConfig('version: 2'); + const result = validateDependabot(); + expect(result.ok).toBe(false); + expect(result.issues).toContain('missing updates array'); + }); + + it('fails when no groups or multi-ecosystem groups', () => { + writeConfig(`version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily +`); + const result = validateDependabot(); + expect(result.ok).toBe(false); + expect(result.issues).toContain('no groups or multi-ecosystem groups defined'); + }); + + it('passes when top-level groups defined', () => { + writeConfig(`version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly +groups: + "all": + patterns: + - "*" +`); + const result = validateDependabot(); + expect(result.ok).toBe(true); + }); + + it('passes when an update has groups', () => { + writeConfig(`version: 2 +updates: + - package-ecosystem: maven + directory: "/" + schedule: + interval: monthly + groups: + "java": + patterns: + - "org.apache.*" +`); + const result = validateDependabot(); + expect(result.ok).toBe(true); + }); + + it('passes when multi-ecosystem groups are used', () => { + writeConfig(`version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: weekly + multi-ecosystem-groups: + "base": + patterns: + - "*" +`); + const result = validateDependabot(); + expect(result.ok).toBe(true); + }); +});