Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
54 changes: 54 additions & 0 deletions action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +19 to +22

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() {
Expand All @@ -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})
Expand Down
58 changes: 58 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"
Comment on lines +23 to +46

# 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)
"