From 1fe9ab8801245737c340e48f58bb5167d6ccc26e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:57:49 +0000 Subject: [PATCH] Add schema validation for humans.txt.yaml --- README.md | 19 ++++++++++++++++++ action.js | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ test.sh | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/README.md b/README.md index e138fed..553c782 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,22 @@ Options: `txt`, `json`, `shell` ## humans.txt This automatically deploys to https://actions.github.io/humans.txt. + +## Schema + +Each entry in `humans.txt.yaml` must conform to the following schema. The file is validated on every run; invalid entries cause a non-zero exit and a descriptive error message. + +```yaml +humans: + - name: "Full Name" # required – non-empty string + alum: true # optional boolean – true if the person no longer works on Actions + honorary_human: true # optional boolean – reserved for special non-human contributors +``` + +| Field | Type | Required | Description | +|---|---|---|---| +| `name` | string | ✅ | The person's full name | +| `alum` | boolean | ❌ | `true` if the person is an alumnus | +| `honorary_human` | boolean | ❌ | `true` for honorary members | + +Unknown fields will cause a validation error. diff --git a/action.js b/action.js index c5adec4..11000a4 100644 --- a/action.js +++ b/action.js @@ -11,6 +11,51 @@ const formatters = { shell: (data, opts) => txtFormatter(data, { ...opts, colors: true}), } +const KNOWN_FIELDS = new Set(['name', 'alum', 'honorary_human']) + +function validateSchema(data) { + const errors = [] + + if (!data || typeof data !== 'object') { + errors.push("root must be a YAML mapping") + return errors + } + + if (!Array.isArray(data.humans)) { + errors.push("'humans' must be a list") + return errors + } + + data.humans.forEach((human, i) => { + const prefix = `humans[${i}]` + + if (typeof human !== 'object' || human === null) { + errors.push(`${prefix} must be a mapping`) + return + } + + if (typeof human.name !== 'string' || human.name.trim() === '') { + errors.push(`${prefix}.name must be a non-empty string`) + } + + if ('alum' in human && typeof human.alum !== 'boolean') { + errors.push(`${prefix}.alum must be a boolean`) + } + + if ('honorary_human' in human && typeof human.honorary_human !== 'boolean') { + errors.push(`${prefix}.honorary_human must be a boolean`) + } + + for (const key of Object.keys(human)) { + if (!KNOWN_FIELDS.has(key)) { + errors.push(`${prefix} has unknown field '${key}'`) + } + } + }) + + return errors +} + main() function main() { @@ -25,6 +70,15 @@ function main() { const data = yaml.parse(fs.readFileSync(__dirname + "/humans.txt.yaml", {encoding: "utf8"})) + const errors = validateSchema(data) + if (errors.length > 0) { + for (const err of errors) { + console.error(`Schema error: ${err}`) + } + process.exitCode = 1 + return + } + data.humans = data.humans.sort((a,b) => a.name > b.name ? 1 : -1) formatter(data, {output}) diff --git a/test.sh b/test.sh index f99dd3e..7f318e7 100755 --- a/test.sh +++ b/test.sh @@ -11,3 +11,61 @@ node action.js html node action.js txt /tmp/hu-output grep "Current humans" /tmp/hu-output +# Schema validation: valid file should succeed +node action.js txt >/dev/null + +# Schema validation: missing name field should fail +cat > /tmp/invalid-humans.yaml << 'EOF' +humans: + - alum: true +EOF +# Use a wrapper to test validation against an invalid file +node -e " +const fs = require('fs') +const yaml = require('yaml') +const data = yaml.parse(fs.readFileSync('/tmp/invalid-humans.yaml', 'utf8')) +// inline the validation function +const KNOWN_FIELDS = new Set(['name','alum','honorary_human']) +function validateSchema(data) { + const errors = [] + if (!data || typeof data !== 'object') { errors.push('root must be a YAML mapping'); return errors } + if (!Array.isArray(data.humans)) { errors.push(\"'humans' must be a list\"); return errors } + data.humans.forEach((human, i) => { + const prefix = 'humans[' + i + ']' + if (typeof human !== 'object' || human === null) { errors.push(prefix + ' must be a mapping'); return } + if (typeof human.name !== 'string' || human.name.trim() === '') errors.push(prefix + '.name must be a non-empty string') + if ('alum' in human && typeof human.alum !== 'boolean') errors.push(prefix + '.alum must be a boolean') + if ('honorary_human' in human && typeof human.honorary_human !== 'boolean') errors.push(prefix + '.honorary_human must be a boolean') + for (const key of Object.keys(human)) { if (!KNOWN_FIELDS.has(key)) errors.push(prefix + \" has unknown field '\" + key + \"'\") } + }) + return errors +} +const errs = validateSchema(data) +if (errs.length === 0) { console.error('Expected validation errors but got none'); process.exit(1) } +console.log('Validation correctly rejected invalid data:', errs) +" + +# Schema validation: unknown field should be rejected +node -e " +const fs = require('fs') +const yaml = require('yaml') +const data = yaml.parse('humans:\n - name: Test\n unknown_field: true\n') +const KNOWN_FIELDS = new Set(['name','alum','honorary_human']) +function validateSchema(data) { + const errors = [] + if (!data || typeof data !== 'object') { errors.push('root must be a YAML mapping'); return errors } + if (!Array.isArray(data.humans)) { errors.push(\"'humans' must be a list\"); return errors } + data.humans.forEach((human, i) => { + const prefix = 'humans[' + i + ']' + if (typeof human !== 'object' || human === null) { errors.push(prefix + ' must be a mapping'); return } + if (typeof human.name !== 'string' || human.name.trim() === '') errors.push(prefix + '.name must be a non-empty string') + if ('alum' in human && typeof human.alum !== 'boolean') errors.push(prefix + '.alum must be a boolean') + if ('honorary_human' in human && typeof human.honorary_human !== 'boolean') errors.push(prefix + '.honorary_human must be a boolean') + for (const key of Object.keys(human)) { if (!KNOWN_FIELDS.has(key)) errors.push(prefix + \" has unknown field '\" + key + \"'\") } + }) + return errors +} +const errs = validateSchema(data) +if (errs.length === 0) { console.error('Expected validation errors for unknown field but got none'); process.exit(1) } +console.log('Validation correctly rejected unknown field:', errs) +"