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
35 changes: 24 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,8 @@ name: CI

on:
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
- 'LICENSE*'
- 'NOTICE*'
push:
branches: [main]
paths-ignore:
- 'docs/**'
- '*.md'
- 'LICENSE*'
- 'NOTICE*'

permissions:
contents: read
Expand All @@ -28,11 +18,31 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
fetch-depth: 0
- name: Classify the change before the full build
id: scope
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
set -euo pipefail
if ! [[ "$BASE_SHA" =~ ^[0-9a-f]{40}$ && \
"$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] || \
[ "$BASE_SHA" = '0000000000000000000000000000000000000000' ]; then
printf 'full_build=true\n' >> "$GITHUB_OUTPUT"
echo 'The comparison range is incomplete. The full build will run.'
exit 0
fi
git diff --name-only "$BASE_SHA" "$HEAD_SHA" | \
node scripts/classify-ci-scope.mjs
- if: steps.scope.outputs.full_build == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
cache: npm
- name: Build and run browser contract
if: steps.scope.outputs.full_build == 'true'
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
env:
# GitHub data is fetched server-side in getStaticProps. The
Expand All @@ -45,3 +55,6 @@ jobs:
wait-on: http://127.0.0.1:3000
wait-on-timeout: 120
config: baseUrl=http://127.0.0.1:3000
- name: Record a documentation-only pass
if: steps.scope.outputs.full_build == 'false'
run: echo 'Only documentation or notice files changed. The required check reported without starting the browser build.'
29 changes: 29 additions & 0 deletions scripts/classify-ci-scope.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { appendFileSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'

const cheapPath = /^(?:docs\/.*|.*\.md|LICENSE(?:\..*)?|NOTICE(?:\..*)?)$/

export function requiresFullBuild(paths) {
if (paths.length === 0) return true
return paths.some((path) => !cheapPath.test(path))
}

function main() {
const paths = readFileSync(0, 'utf8')
.split('\n')
.map((path) => path.trim())
.filter(Boolean)
const fullBuild = requiresFullBuild(paths)
const output = process.env.GITHUB_OUTPUT
if (!output) throw new Error('GITHUB_OUTPUT is required')
appendFileSync(output, `full_build=${fullBuild}\n`)
console.log(
fullBuild
? 'A product, workflow, or build file changed. The full build will run.'
: 'Only documentation or notice files changed. The full build can be skipped.'
)
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main()
}
41 changes: 41 additions & 0 deletions tests/ciWorkflowContract.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const fs = require('node:fs')
const path = require('node:path')
const test = require('node:test')
const assert = require('node:assert/strict')

const root = path.resolve(__dirname, '..')
const workflow = fs.readFileSync(
path.join(root, '.github/workflows/ci.yml'),
'utf8'
)

test('build-and-e2e reports on every pull request', () => {
const pullRequest = workflow
.split(' pull_request:', 2)[1]
.split(' push:', 1)[0]
assert.doesNotMatch(pullRequest, /paths(?:-ignore)?:/)
assert.match(workflow, /^ build-and-e2e:$/m)
})

test('the full browser contract stays behind a fail-closed classifier', () => {
assert.match(workflow, /fetch-depth: 0/)
assert.match(workflow, /git diff --name-only "\$BASE_SHA" "\$HEAD_SHA"/)
assert.match(workflow, /node scripts\/classify-ci-scope\.mjs/)
assert.match(
workflow,
/if: steps\.scope\.outputs\.full_build == 'true'\n\s+uses: cypress-io\/github-action@/
)
assert.match(workflow, /BASE_SHA.*0000000000000000000000000000000000000000/s)
assert.match(workflow, /printf 'full_build=true\\n'/)
})

test('the classifier skips only documentation and notice files', async () => {
const { requiresFullBuild } = await import(
'../scripts/classify-ci-scope.mjs'
)
assert.equal(requiresFullBuild(['docs/operator.md', 'README.md']), false)
assert.equal(requiresFullBuild(['LICENSE', 'NOTICE.txt']), false)
assert.equal(requiresFullBuild(['docs/operator.md', 'components/Hero.js']), true)
assert.equal(requiresFullBuild(['.github/workflows/ci.yml']), true)
assert.equal(requiresFullBuild([]), true)
})
Loading