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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,14 @@
},
{
"run": "npm pack"
},
{
"uses": "actions/upload-artifact@v7.0.1",
"with": {
"name": "package-tarball",
"path": "*.tgz",
"if-no-files-found": "error"
}
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions fjs/ci/config/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export const actions = /** @type {const} */({
'actions/setup-node': 'v7.0.0',
// https://github.com/marketplace/actions/cache
'actions/cache': 'v6.1.0',
// https://github.com/marketplace/actions/upload-a-build-artifact
'actions/upload-artifact': 'v7.0.1',
// https://github.com/marketplace/actions/setup-deno
'denoland/setup-deno': 'v2.0.5',
// https://github.com/marketplace/actions/setup-bun
Expand Down
16 changes: 16 additions & 0 deletions fjs/ci/node/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { node } from '../config/module.f.mjs'
import { install, test, ubuntuArm, uses } from '../common/module.f.mjs'
import { nixInstall, nixVersionCheckStep } from '../nix/module.f.mjs'

/**
* Name of the CI artifact carrying the `npm pack` tarball. The producing step
* is below; a consuming job downloads it by this name.
*/
export const packageArtifact = /** @type {const} */ ('package-tarball')

/** @type {(v: string) => string} */
export const major = v => v.split('.')[0]

Expand Down Expand Up @@ -71,6 +77,16 @@ const node26Steps = [
test({ run: 'npx tsc' }),
test({ run: 'npm run cov' }),
test({ run: 'npm pack' }),
// Hands the tarball to a job that has no checkout, which is the only place
// the package can be checked as a consumer sees it. `if-no-files-found`
// must be `error`: the default warns and uploads nothing, so a consuming
// job would fail later on a missing artifact rather than here on the real
// cause.
test(uses('actions/upload-artifact', {
name: packageArtifact,
path: '*.tgz',
'if-no-files-found': 'error',
})),
]

/** @type {(steps: readonly MetaStep[]) => Job} */
Expand Down
36 changes: 34 additions & 2 deletions fjs/ci/proof.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

import { exitCode } from '../effects/node/module.f.mjs'
import { ci, main } from './module.f.mjs'
import { functionalscript, node } from './config/module.f.mjs'
import { nodeNixJobs } from './node/module.f.mjs'
import { actions, functionalscript, node } from './config/module.f.mjs'
import { major, nodeNixJobs, packageArtifact } from './node/module.f.mjs'
import { utf8, utf8ToString } from '../text/module.f.mjs'
import { empty as emptyVec } from '../types/bit_vec/module.f.mjs'
import { test, ubuntu, parseGitHubAction } from './common/module.f.mjs'
Expand Down Expand Up @@ -222,6 +222,38 @@ export const proof = {
assert(job['runs-on'] !== undefined, 'expected runs-on')
assert(job.steps.length > 0, 'expected steps')
},
packageArtifact: () => {
const gha = run(false)
const job = gha.jobs[`node${major(node.default)}`]
assert(job !== undefined, 'expected the canonical Node job')
const packIndex = job.steps.findIndex(step => step.run === 'npm pack')
const uploadIndex = job.steps.findIndex(
step => step.uses === `actions/upload-artifact@${actions['actions/upload-artifact']}`)
assert(packIndex !== -1, 'expected npm pack')
assert(uploadIndex !== -1, 'expected the artifact upload')
// Uploading before packing would ship an empty artifact, and the
// failure would then surface in the consuming job rather than here,
// where the cause is.
assert(uploadIndex > packIndex, 'expected the upload to follow npm pack')
const upload = job.steps[uploadIndex]?.with
// Producer and consumer share the exported name rather than repeating
// a string literal that can drift apart.
assertEq(upload?.name, packageArtifact)
// The glob has to match what `npm pack` writes. `if-no-files-found`
// catches a glob that matches *nothing*; a glob matching the *wrong*
// files would upload them quietly, so pin it.
assertEq(upload?.path, '*.tgz')
// The action's default is to warn and upload nothing, which would make
// a packing failure look like a consumer bug.
assertEq(upload?.['if-no-files-found'], 'error')
// One producer: a second upload under the same name is a race, not
// redundancy.
assertEq(
definedValues(gha.jobs).filter(j =>
j.steps.some(step => step.uses?.startsWith('actions/upload-artifact@') === true)).length,
1,
'expected exactly one job to upload the package')
},
jobNeeds: () => {
const steps = /** @type {const} */ ([{ run: 'echo hi' }])
/** @type {(jobs: Unknown) => Unknown} */
Expand Down
Loading