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
4 changes: 4 additions & 0 deletions .github/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ Feature gates queued for stabilization (`@unstable` → `@since`) must have
passed their phase 3 vote, and any new dependence on a Component Model feature
must have been adopted by a WASI Subgroup vote, before the release is cut. Both
processes are documented in [CONTRIBUTING.md](../CONTRIBUTING.md).

Adopted Component Model features are recorded in [Component Model features in
WASI](../docs/ComponentModelFeatures.md); features newly adopted for a release
must be called out in that release's notes.
2 changes: 1 addition & 1 deletion .github/actions/install-tools/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ inputs:
wasm-tools-version:
description: 'Version of wasm-tools to install'
required: false
default: '1.244.0'
default: '1.256.0'

runs:
using: 'composite'
Expand Down
15 changes: 15 additions & 0 deletions .github/scripts/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# CI Scripts

## cm-features.js

Reads the Component Model adoption record in `docs/ComponentModelFeatures.md`,
which is the single source of truth for which gated features WASI requires.

- `publish.yml` runs it to write the "Component Model features" section of each
released `specifications/wasi-<version>/Overview.md`.
- `validate-proposals.js` imports `readRecord` from it to build the
`wasm-tools validate` feature flags.

```bash
# Print the section for a given version
node .github/scripts/cm-features.js 0.3.1
```

## validate-proposals.js

Validates WIT definitions for changed proposals. Used by the CI workflow.
Expand Down
137 changes: 137 additions & 0 deletions .github/scripts/cm-features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env node

// Emit the "Component Model features" section of a specification overview.
//
// The adoption record in docs/ComponentModelFeatures.md is the source of
// truth: it lists every Component Model gated feature the WASI Subgroup has
// voted to adopt, and the release it was adopted for. Adoption is cumulative,
// so the section for a given version lists every feature adopted in that
// version or an earlier one.
//
// Prerelease versions (`0.3.0-rc-*`) are treated as their base version, since
// a release candidate carries the same feature baseline as the release it is
// a candidate for.
//
// Usage: node cm-features.js <version> [record-path]

const fs = require('fs');
const path = require('path');

const RECORD = path.join(__dirname, '..', '..', 'docs', 'ComponentModelFeatures.md');
const HEADING = '## Adopted features';

// The record marks the default (ungated) features with an em dash in the gate
// column. They are not a gated feature, so they are described in prose below
// rather than listed as a row.
const UNGATED = '—';

function parseVersion(version) {
const base = version.split('-')[0];
const parts = base.split('.').map((part) => Number(part));
if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) {
throw new Error(`not a version: '${version}'`);
}
return parts;
}

function compareVersions(a, b) {
const left = parseVersion(a);
const right = parseVersion(b);
for (let i = 0; i < 3; i++) {
if (left[i] !== right[i]) return left[i] - right[i];
}
return 0;
}

// Rows of the adoption table, in document order, as {version, gate, feature}.
function readRecord(recordPath = RECORD) {
const lines = fs.readFileSync(recordPath, 'utf-8').split('\n');
const start = lines.indexOf(HEADING);
if (start === -1) {
throw new Error(`${recordPath}: no '${HEADING}' heading`);
}

const rows = [];
for (let i = start + 1; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('## ')) break;
if (!line.startsWith('|')) continue;
// Cells are read positionally, so a row missing its trailing pipe would
// silently lose the last character of its final cell.
if (!line.endsWith('|')) {
throw new Error(`${recordPath}:${i + 1}: table row does not end with '|'`);
}

const cells = line.slice(1, -1).split('|').map((cell) => cell.trim());
if (cells.length !== 4) {
throw new Error(`${recordPath}:${i + 1}: expected 4 columns, found ${cells.length}`);
}
// Skip the header and its separator, in any of the alignment styles.
if (cells[0] === 'WASI version') continue;
if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue;

rows.push({ version: cells[0], gate: cells[1], feature: cells[2] });
}

if (rows.length === 0) {
throw new Error(`${recordPath}: no rows under '${HEADING}'`);
}
return rows;
}

// The section is appended to a partially written overview, ahead of that
// file's link definitions, so it both starts and ends with a blank line: a
// link definition on the line directly below a paragraph would be parsed as
// part of that paragraph.
function render(version, rows) {
const adopted = rows
.filter((row) => row.gate !== UNGATED)
.filter((row) => compareVersions(row.version, version) <= 0);

const out = ['', '## Component Model features', ''];

if (adopted.length === 0) {
out.push(
'Implementing this version of the specification requires the default (ungated)',
'[Component Model][cm] features. No [gated features][gates] have been adopted.',
'',
''
);
return out.join('\n');
}

out.push(
'Implementing this version of the specification requires the default (ungated)',
'[Component Model][cm] features, plus the [gated features][gates] adopted by the',
'WASI Subgroup up to and including this version:',
'',
'| Gate | Feature | Adopted in |',
'| --- | --- | --- |'
);
for (const row of adopted) {
out.push(`| ${row.gate} | ${row.feature} | ${row.version} |`);
}
out.push(
'',
'These are required whether or not an API in this version uses them. See',
'[Component Model features in WASI][features] for the full adoption record.',
'',
''
);
return out.join('\n');
}

function main() {
const [version, recordPath = RECORD] = process.argv.slice(2);
if (!version) {
console.error('Usage: node cm-features.js <version> [record-path]');
process.exit(1);
}
process.stdout.write(render(version, readRecord(recordPath)));
}

if (require.main === module) {
main();
}

module.exports = { RECORD, UNGATED, readRecord, render, compareVersions };
53 changes: 52 additions & 1 deletion .github/scripts/validate-proposals.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,45 @@

const { execSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { validateDirectory, formatErrors } = require('./validate-since');
const { readRecord, UNGATED } = require('./cm-features');

// `wasm-tools` feature name for each Component Model gate WASI has adopted.
// Adding a row to docs/ComponentModelFeatures.md without an entry here is a
// hard error rather than a silently un-enforced feature.
const GATE_FEATURES = {
'🔀': 'cm-async',
'🗺️': 'cm-map',
'🏷️': 'cm-implements',
};

// Emoji gates carry a variation selector (U+FE0F) inconsistently across
// sources, so compare them without it.
const bare = (gate) => gate.replace(/\uFE0F/g, '');

// The adopted Component Model features, as `wasm-tools` CLI flags. Enabling
// them is what keeps proposals from depending on a feature the Subgroup has
// not voted to adopt: an un-adopted gate stays off and fails validation. Note
// these are added to whatever `wasm-tools` enables by default, so a gate that
// is un-adopted here but on by default there is not caught.
const adoptedFeatureFlags = () => {
const lookup = new Map(Object.entries(GATE_FEATURES).map(([g, f]) => [bare(g), f]));
const features = new Set();
for (const { gate, version } of readRecord()) {
if (gate === UNGATED) continue;
const feature = lookup.get(bare(gate));
if (!feature) {
throw new Error(
`docs/ComponentModelFeatures.md adopts '${gate}' for ${version}, but ` +
`GATE_FEATURES in ${path.basename(__filename)} has no wasm-tools feature name for it`
);
}
features.add(feature);
}
return [...features].map((feature) => `-f ${feature}`).join(' ');
};

const parseFiles = (filesJson) => {
if (!filesJson || filesJson === 'null') return [];
Expand Down Expand Up @@ -48,6 +86,10 @@ if (toValidate.length === 0) {

let failed = false;

const featureFlags = adoptedFeatureFlags();
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wasi-validate-'));
console.log(`Component Model features: ${featureFlags || '(none adopted)'}`);

for (const proposal of toValidate) {
const witDir = ((proposal) => `proposals/${proposal}/wit`)(proposal);
console.log(`::group::Validating ${proposal}`);
Expand Down Expand Up @@ -81,9 +123,16 @@ for (const proposal of toValidate) {

// Validate wasm encoding
console.log(' Validating wasm encoding...');
if (!run(`wasm-tools component wit "${witDir}" --wasm -o /dev/null`)) {
const encoded = path.join(outDir, `${proposal}.wasm`);
if (!run(`wasm-tools component wit "${witDir}" --wasm -o "${encoded}"`)) {
console.log(`::error::wasm encoding failed for ${proposal}`);
failed = true;
} else if (!run(`wasm-tools validate ${featureFlags} "${encoded}"`)) {
// `component wit --wasm` does not enforce Component Model feature gates,
// so the encoded package is validated separately against the features
// WASI has adopted.
console.log(`::error::Component Model validation failed for ${proposal}`);
failed = true;
}

// Validate @since annotations
Expand All @@ -99,6 +148,8 @@ for (const proposal of toValidate) {
}
}

fs.rmSync(outDir, { recursive: true, force: true });

if (failed) {
console.log('\n❌ Validation failed');
process.exit(1);
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,18 @@ jobs:
- [wasi:filesystem@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Ffilesystem?tag=${VERSION})
- [wasi:cli@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fcli?tag=${VERSION})
- [wasi:http@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fhttp?tag=${VERSION})
EOF

# Which Component Model gated features this version requires, from
# the adoption record in docs/ComponentModelFeatures.md.
node .github/scripts/cm-features.js "$VERSION" >> "$SPEC_DIR/Overview.md"

cat >> "$SPEC_DIR/Overview.md" << EOF
[cm]: https://github.com/WebAssembly/component-model
[wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md
[wasm-oci]: https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact
[gates]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features
[features]: https://github.com/WebAssembly/WASI/blob/main/docs/ComponentModelFeatures.md
EOF

# Remove leading whitespace from heredoc
Expand Down
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,20 @@ followed:
evaluates whether the feature is genuinely stable and whether the iteration
and feedback phase has been sufficient.
1. Once the SG votes to adopt the feature, stable (`@since`-gated) WASI APIs in
the next release may depend on it. The release notes for that version must
call out the newly adopted Component Model feature.
the next release may depend on it. The adoption is recorded in
[Component Model features in WASI](docs/ComponentModelFeatures.md), and the
release notes for that version must call out the newly adopted Component
Model feature.

**Note:** Not all Component Model features are surfaced in WASI WIT
definitions. Some, such as new canonical ABI features, have no WIT-level
syntax. Even in those cases, these features follow the same process.
1. From that release onward, the feature is part of the baseline set of
Component Model features required to implement that WASI version, matching
the feature's status in the Component Model's [gated features] documentation
(for example, WASI 0.3 adopts the features gated by 🔀 async).
(for example, WASI 0.3 adopts the features gated by 🔀 async). A PR to the
Component Model repository updates that documentation to record which WASI
release adopted the feature.

[Component Model]: https://github.com/WebAssembly/component-model
[gated features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features
41 changes: 41 additions & 0 deletions docs/ComponentModelFeatures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Component Model features in WASI

WASI APIs are defined in terms of the [Component Model], which develops new WIT
syntax, types, and canonical ABI functionality behind [gated features]. Stable
(`@since`-gated) WASI APIs may only depend on a gated feature after the WASI
Subgroup has voted to adopt it, following the process described in
[CONTRIBUTING.md](../CONTRIBUTING.md#adopting-component-model-features).

This document records which features have been adopted, and the release they
were adopted for. Adoption is cumulative: implementing a given WASI version
requires the default (ungated) Component Model features, plus every feature
adopted in that version and in all earlier ones.

Adopting a feature does not mean that any WASI API uses it. It means that WASI
APIs in that release and later *may* use it, and that runtimes and toolchains
which implement that WASI version must implement the feature regardless of
whether it is used yet.

## Adopted features

This table is the source for the "Component Model features" section of each
released [specification overview](../specifications).

| WASI version | Gate | Feature | Adopted |
| --- | --- | --- | --- |
| 0.2.0 | — | The default (ungated) Component Model features | WASI 0.2 vote (predates this process) |
| 0.3.0 | 🔀 | `async` lift/lower, `future` and `stream` | WASI 0.3 vote (predates this process) |
| 0.3.1 | 🗺️ | The `map<K, V>` type | [2026-08-06](https://github.com/WebAssembly/meetings/blob/main/wasi/2026/WASI-08-06.md) ([#943](https://github.com/WebAssembly/WASI/issues/943)) |
| 0.3.1 | 🏷️ | `implements` and `external-id` annotations on plain-named interface imports and exports | [2026-08-06](https://github.com/WebAssembly/meetings/blob/main/wasi/2026/WASI-08-06.md) ([#942](https://github.com/WebAssembly/WASI/issues/942)) |

Features which have not been adopted are listed under [gated features] in the
Component Model Explainer. WASI proposals may experiment with them in
prerelease versions (such as `0.3.0-rc-*` releases), but stable releases must
remain implementable without them.

CI enforces this: each changed proposal is encoded to a component and validated
with the features in the table above enabled, so a proposal that depends on an
un-adopted feature fails validation.

[Component Model]: https://github.com/WebAssembly/component-model
[gated features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features
3 changes: 3 additions & 0 deletions docs/Release.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@ workflow which begin the release process. This process has the following steps:
1. Trigger the Release WASI workflow by running `gh workflow run release.yml -f prev_version=0.3.<prev> -f next_version=0.3.<next>`.
1. A PR updating all version numbers is created. This is blocked on CI and review ([example](https://github.com/WebAssembly/WASI/pull/924)).
2. Create a GitHub release by running `gh release create "v0.3.<version>" --generate-notes`.
If the release adopts new Component Model features, the generated notes must
be edited to call them out. See [Component Model features in
WASI](ComponentModelFeatures.md).
3. Once the release has been created, a second PR will be automatically created to populate the `specifications/` dir ([example](https://github.com/WebAssembly/WASI/pull/925)).
4. Once this final PR is merged the release process is complete.
15 changes: 15 additions & 0 deletions specifications/wasi-0.3.0/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ layout][wasm-oci]:
- [wasi:cli@0.3.0](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fcli?tag=0.3.0)
- [wasi:http@0.3.0](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fhttp?tag=0.3.0)

## Component Model features

Implementing this version of the specification requires the default (ungated)
[Component Model][cm] features, plus the [gated features][gates] adopted by the
WASI Subgroup up to and including this version:

| Gate | Feature | Adopted in |
| --- | --- | --- |
| 🔀 | `async`, `future` and `stream` | 0.3.0 |

These are required whether or not an API in this version uses them. See
[Component Model features in WASI][features] for the full adoption record.

[cm]: https://github.com/WebAssembly/component-model
[wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md
[wasm-oci]: https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact
[gates]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features
[features]: https://github.com/WebAssembly/WASI/blob/main/docs/ComponentModelFeatures.md