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
2 changes: 1 addition & 1 deletion .github/workflows/accessibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
node-version: 22

# In your own repository this becomes:
# uses: likeBloodMoon/eaa-kit@v0.9.0
# uses: likeBloodMoon/eaa-kit@v0.9.1
#
# An exact release tag. There is deliberately no moving v0 tag to follow:
# this is a 0.x package, the flags and the JSON contract can still move
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,21 @@ jobs:
run: |
pnpm examples
git diff --exit-code examples/

# A release PR is one that changes the version. It gets the checks the
# tag will need, while they can still be fixed in the PR: v0.8.0 was
# tagged on a commit that still said 0.7.0, and the release workflow only
# noticed after the tag was public.
- name: Check a release PR is ready to tag
if: github.event_name == 'pull_request' && runner.os == 'Linux' && matrix.node-version == 24
env:
BASE_REF: ${{ github.base_ref }}
run: |
git fetch --no-tags --depth=1 origin "$BASE_REF"
base="$(git show "origin/$BASE_REF:package.json" | node -p "JSON.parse(require('fs').readFileSync(0, 'utf8')).version")"
head="$(node -p "require('./package.json').version")"
if [ "$base" != "$head" ]; then
pnpm release:check
else
echo "Version unchanged at $head: not a release PR."
fi
13 changes: 5 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,11 @@ jobs:
- run: pnpm build
- run: pnpm smoke

- name: Check the tag matches package.json
run: |
tag="${GITHUB_REF_NAME#v}"
pkg="$(node -p "require('./package.json').version")"
if [ "$tag" != "$pkg" ]; then
echo "::error::tag v${tag} does not match package.json version ${pkg}"
exit 1
fi
# The tag against package.json, the dated changelog, the Action pins
# and the examples. CI already ran this on the release PR; running it
# again here covers a tag pushed by hand onto some other commit.
- name: Check this commit is ready to publish as this tag
run: pnpm release:check "$GITHUB_REF_NAME"

# access and provenance come from publishConfig in package.json, so a
# manual publish behaves the same way.
Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ move: the JSON report's `schemaVersion` and the baseline file's. Both are bumped
a field is removed, renamed, or changes meaning — new fields may appear without one, so
consumers must ignore what they do not recognise.

## Unreleased
## 0.9.1 — 2026-09-26

### Fixed

Expand All @@ -32,6 +32,15 @@ consumers must ignore what they do not recognise.
A statement generated with 0.9.0 for Denmark, Czechia or Portugal should be generated
again.

### Added

- **`pnpm release:check`**, for maintainers: what has to be true before a version is
tagged (the tag matches `package.json`, the changelog has a dated heading and no
"Unreleased" section, the Action is pinned to this version in the docs, and the examples
were generated by it). CI runs it on any PR that changes the version, and the release
workflow runs it against the tag before publishing, so a tag like v0.8.0, cut on a commit
that still said 0.7.0, is caught before it is pushed.

## 0.9.0 — 2026-09-24

The first release since 0.7.0, and it carries 0.8.0 as well: 0.8.0 was tagged but never
Expand Down
428 changes: 424 additions & 4 deletions ROADMAP.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: likeBloodMoon/eaa-kit@v0.9.0
- uses: likeBloodMoon/eaa-kit@v0.9.1
with:
install-command: npm ci
build-command: npm run build
Expand Down
4 changes: 2 additions & 2 deletions examples/report.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/report.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/report.sarif

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/statement.de.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eaa-kit",
"version": "0.9.0",
"version": "0.9.1",
"description": "WCAG 2.2 AA auditor and EU accessibility statement generator for fifteen countries (AT, BE, CH, CZ, DE, DK, ES, FI, FR, IE, IT, NL, PL, PT, SE). Audits a static build or a running site from the command line.",
"type": "module",
"license": "MIT",
Expand Down Expand Up @@ -82,7 +82,8 @@
"smoke": "pnpm build && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json && node dist/cli/index.js audit tests/fixtures/site --include \"about/**\" \"blog/**\" \"drafts/**\" \"legacy.htm\" --concurrency 2 && node dist/cli/index.js audit tests/fixtures/site --include about/** --format html && node dist/cli/index.js audit tests/fixtures/site --baseline examples/baseline.json && node -e \"import('./dist/astro/index.js').then(m => { const i = m.default(); if (i.name !== 'eaa-kit' || typeof i.hooks['astro:build:done'] !== 'function') { throw new Error('astro entry point is not an integration') } console.log('astro entry ok') })\" && node dist/cli/index.js baseline tests/fixtures/site --output .eaa-kit/smoke-baseline.json && node dist/cli/index.js baseline tests/fixtures/site --output .eaa-kit/smoke-baseline.json --prune && node dist/cli/index.js checklist --record .eaa-kit/smoke-review.json --output .eaa-kit/smoke-review.md && node dist/cli/index.js audit tests/fixtures/site --include about/** --review .eaa-kit/smoke-review.json --coverage && node dist/cli/index.js statement --config examples/eaa.config.json && node dist/cli/index.js statement --config examples/eaa.config.json --audit examples/report.json --format html && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --no-cache --output .eaa-kit/smoke-base.json && node dist/cli/index.js audit tests/fixtures/site --include about/** --format json --no-cache --output .eaa-kit/smoke-head.json && node dist/cli/index.js diff .eaa-kit/smoke-base.json .eaa-kit/smoke-head.json --format json",
"bench": "pnpm build && node scripts/bench.mjs",
"examples": "pnpm build && node scripts/generate-examples.mjs",
"test:packaged": "pnpm build && node scripts/test-packaged.mjs"
"test:packaged": "pnpm build && node scripts/test-packaged.mjs",
"release:check": "node scripts/release-check.ts"
},
"peerDependencies": {
"astro": ">=4.0.0",
Expand Down
99 changes: 99 additions & 0 deletions scripts/release-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env node
// What has to be true before a version is tagged. Run with `pnpm release:check`.
//
// v0.8.0 was tagged on a commit whose package.json still said 0.7.0. The
// release workflow caught that, but only after the tag was public, and a
// public tag is awkward to take back. Every item here has either gone wrong
// once or had to be remembered by hand at every release, so the list lives in
// code instead of in somebody's head, and CI runs it on the release PR,
// before anything is tagged.
//
// Pass the tag to compare against, as the release workflow does. Without one,
// the check is about the working tree alone.

import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

export interface ReleaseCheckOptions {
root: string
/** The tag being released, `v1.2.3`. Optional: a release PR has no tag yet. */
tag?: string
}

/** Where the Action is pinned to an exact release. */
const PIN_FILES = ['README.md', 'docs', '.github/workflows']

const PIN = /likeBloodMoon\/eaa-kit@v(\d+\.\d+\.\d+(?:-[\w.]+)?)/g

/** Every problem that would make this commit the wrong one to tag, in words. */
export async function checkRelease(options: ReleaseCheckOptions): Promise<string[]> {
const problems: string[] = []
const read = (file: string): Promise<string> => readFile(path.join(options.root, file), 'utf8')

const version = (JSON.parse(await read('package.json')) as { version: string }).version

if (options.tag !== undefined && options.tag !== `v${version}`) {
problems.push(`the tag is ${options.tag}, but package.json says ${version}`)
}

const changelog = await read('CHANGELOG.md')
if (/^## Unreleased\b/m.test(changelog)) {
problems.push(
'CHANGELOG.md still has an "Unreleased" section: date it, or move it under the version',
)
}
const heading = new RegExp(`^## ${version.replaceAll('.', '\\.')} — (\\S+)`, 'm').exec(changelog)
if (heading === null) {
problems.push(`CHANGELOG.md has no "## ${version} — <date>" heading`)
} else if (!/^\d{4}-\d{2}-\d{2}$/.test(heading[1] ?? '')) {
problems.push(`the CHANGELOG.md heading for ${version} is not dated as YYYY-MM-DD`)
}

for (const file of await markdownAndWorkflows(options.root)) {
const text = await read(file)
for (const match of text.matchAll(PIN)) {
if (match[1] !== version) {
problems.push(`${file} pins the Action to v${match[1]}, not v${version}`)
}
}
}

// The examples stamp the version, so a stale one means `pnpm examples` was
// not re-run, and whatever else it would have changed has not been looked at.
const report = JSON.parse(await read('examples/report.json')) as { tool: { version: string } }
if (report.tool.version !== version) {
problems.push(
`examples/ was generated by ${report.tool.version}: run pnpm examples and commit the result`,
)
}

return problems
}

async function markdownAndWorkflows(root: string): Promise<string[]> {
const files: string[] = []
for (const entry of PIN_FILES) {
const full = path.join(root, entry)
const listed = await readdir(full, { recursive: true }).catch(() => undefined)
if (listed === undefined) {
files.push(entry)
continue
}
for (const name of listed) {
if (/\.(md|ya?ml)$/.test(name)) files.push(path.join(entry, name))
}
}
return files.sort()
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
const tag = process.argv[2] ?? undefined
const problems = await checkRelease(tag === undefined ? { root: '.' } : { root: '.', tag })
if (problems.length === 0) {
console.log('release:check ok')
} else {
for (const problem of problems) console.error(`✗ ${problem}`)
process.exitCode = 1
}
}
87 changes: 87 additions & 0 deletions tests/release-check.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { checkRelease } from '../scripts/release-check.ts'

const dirs: string[] = []

afterEach(async () => {
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })))
})

/** A repository that is ready to tag as 1.2.3, with any file replaced. */
async function repo(overrides: Record<string, string> = {}): Promise<string> {
const root = await mkdtemp(path.join(tmpdir(), 'eaa-kit-release-'))
dirs.push(root)
const files: Record<string, string> = {
'package.json': JSON.stringify({ version: '1.2.3' }),
'CHANGELOG.md': '# Changelog\n\n## 1.2.3 — 2026-10-01\n\n- Things.\n\n## 1.2.2 — 2026-09-01\n',
'README.md': 'uses: likeBloodMoon/eaa-kit@v1.2.3\n',
'docs/integrations.md': '- uses: likeBloodMoon/eaa-kit@v1.2.3\n',
'.github/workflows/accessibility.yml': '# uses: likeBloodMoon/eaa-kit@v1.2.3\n',
'examples/report.json': JSON.stringify({ tool: { version: '1.2.3' } }),
...overrides,
}
for (const [name, body] of Object.entries(files)) {
await mkdir(path.join(root, path.dirname(name)), { recursive: true })
await writeFile(path.join(root, name), body)
}
return root
}

describe('release:check', () => {
it('passes a repository that is ready, with and without the tag', async () => {
const root = await repo()

expect(await checkRelease({ root })).toEqual([])
expect(await checkRelease({ root, tag: 'v1.2.3' })).toEqual([])
})

it('refuses a tag that does not match package.json, as v0.8.0 was', async () => {
const problems = await checkRelease({ root: await repo(), tag: 'v1.2.4' })

expect(problems).toEqual(['the tag is v1.2.4, but package.json says 1.2.3'])
})

it('refuses a changelog with no heading for the version', async () => {
const root = await repo({ 'CHANGELOG.md': '# Changelog\n\n## 1.2.2 — 2026-09-01\n' })

expect(await checkRelease({ root })).toEqual([
'CHANGELOG.md has no "## 1.2.3 — <date>" heading',
])
})

it('refuses an undated heading and a leftover Unreleased section', async () => {
const root = await repo({
'CHANGELOG.md': '# Changelog\n\n## Unreleased\n\n## 1.2.3 — soon\n',
})

expect(await checkRelease({ root })).toEqual([
'CHANGELOG.md still has an "Unreleased" section: date it, or move it under the version',
'the CHANGELOG.md heading for 1.2.3 is not dated as YYYY-MM-DD',
])
})

it('names every file that pins the Action to another version', async () => {
const root = await repo({
'docs/integrations.md': '- uses: likeBloodMoon/eaa-kit@v1.2.2\n',
'.github/workflows/accessibility.yml': '# uses: likeBloodMoon/eaa-kit@v1.0.0\n',
})

expect(await checkRelease({ root })).toEqual([
`${path.join('.github/workflows', 'accessibility.yml')} pins the Action to v1.0.0, not v1.2.3`,
`${path.join('docs', 'integrations.md')} pins the Action to v1.2.2, not v1.2.3`,
])
})

it('refuses examples generated by another version', async () => {
const root = await repo({
'examples/report.json': JSON.stringify({ tool: { version: '1.2.2' } }),
})

expect(await checkRelease({ root })).toEqual([
'examples/ was generated by 1.2.2: run pnpm examples and commit the result',
])
})
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@

"types": ["node"]
},
"include": ["src", "tests", "*.config.ts"]
"include": ["src", "tests", "scripts/**/*.ts", "*.config.ts"]
}
Loading