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
6 changes: 6 additions & 0 deletions fjs/ci/common/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ export const stepSchema = /** @type {const} */ ({
with: or(option, record(string))
})

// `needs` is how one job waits for another: a job that consumes an artifact
// cannot start before the job that uploads it. It is optional because most jobs
// are independent, and it is named here rather than emitted past the schema —
// `parseGitHubAction` reads back the workflow this repository generates, so an
// unmodelled key would fail that round-trip in `fjs/ci/proof.f.mjs`.
export const jobSchema = /** @type {const} */ ({
'runs-on': string,
needs: or(option, array(string)),
steps: array(stepSchema)
})

Expand Down
36 changes: 36 additions & 0 deletions fjs/ci/proof.f.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @import { MetaStep, Os, GitHubAction } from './common/types.ts'
* @import { Dir, State } from '../effects/node/virtual/types.ts'
* @import { Unknown } from '../djs/types.ts'
*/

import { exitCode } from '../effects/node/module.f.mjs'
Expand Down Expand Up @@ -221,4 +222,39 @@ export const proof = {
assert(job['runs-on'] !== undefined, 'expected runs-on')
assert(job.steps.length > 0, 'expected steps')
},
jobNeeds: () => {
const steps = /** @type {const} */ ([{ run: 'echo hi' }])
/** @type {(jobs: Unknown) => Unknown} */
const action = jobs => ({
name: 'test',
on: {},
permissions: { contents: 'read' },
jobs,
})
// Modelled, so a job that waits for another survives the round-trip.
// Without this a consuming job could only reach the workflow by being
// emitted past the schema, which `parseGitHubAction` would then reject.
const ordered = unwrap(parseGitHubAction(action({
pack: { 'runs-on': 'ubuntu-latest', steps },
check: { 'runs-on': 'ubuntu-latest', needs: ['pack'], steps },
})))
assertEq(ordered.jobs.check?.needs?.[0], 'pack')
assertEq(ordered.jobs.check?.needs?.length, 1)
// Optional: the independent jobs, which is all of them today, still parse.
assertEq(unwrap(parseGitHubAction(action({
pack: { 'runs-on': 'ubuntu-latest', steps },
}))).jobs.pack?.needs, undefined)
// Constrained, not merely accepted. GitHub also allows a bare scalar
// (`needs: pack`); this generator emits the list form only, so the
// scalar is drift rather than an alternative spelling — the same reason
// these schemas are closed.
assertEq(parseGitHubAction(action({
check: { 'runs-on': 'ubuntu-latest', needs: 'pack', steps },
}))[0], 'error')
// Dormant until something orders itself: the first consumer is the
// packed-artifact check in `fjs/ci/todo/f-mjs-package-support.md`.
assert(
definedValues(run(false).jobs).every(job => job.needs === undefined),
'unexpected job ordering in the generated workflow')
},
}
Loading