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
104 changes: 104 additions & 0 deletions .github/scripts/stabilize-features.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env node

// Promote feature gates marked for stabilization during a release.
//
// Convention:
//
// // @stabilize-next
// @unstable(feature = cli-exit-with-code)
// exit-with-code: func(status-code: u8);
//
// The `// @stabilize-next` marker line must sit immediately above an
// `@unstable(feature = ...)` gate. When a release for version NEXT is cut,
// this script rewrites the gate to `@since(version = NEXT)` and removes the
// marker line, so the feature graduates exactly when the package version is
// bumped to NEXT (the only point at which `@since(NEXT)` is legal).
//
// Usage: node stabilize-features.js <wit-dir> <next-version>

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

const MARKER_PATTERN = /^\s*\/\/\s*@stabilize-next\s*$/;
const UNSTABLE_PATTERN = /^(\s*)@unstable\s*\(\s*feature\s*=\s*([a-z][a-z0-9-]*)\s*\)\s*$/i;

function stabilizeFile(filePath, nextVersion) {
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
const out = [];
const promoted = [];

for (let i = 0; i < lines.length; i++) {
if (MARKER_PATTERN.test(lines[i])) {
const next = lines[i + 1];
const m = next && next.match(UNSTABLE_PATTERN);
if (!m) {
throw new Error(
`${filePath}:${i + 1}: '// @stabilize-next' is not immediately followed by an @unstable(...) gate`
);
}
const indent = m[1];
// Drop the marker line, replace the gate on the following line.
out.push(`${indent}@since(version = ${nextVersion})`);
promoted.push({ feature: m[2], line: i + 1 });
i++; // consume the @unstable line we just rewrote
continue;
}
out.push(lines[i]);
}

if (promoted.length > 0) {
fs.writeFileSync(filePath, out.join('\n'));
}
return promoted;
}

function walkWit(dir, cb) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === 'deps') continue; // pulled packages, leave untouched
walkWit(full, cb);
} else if (entry.name.endsWith('.wit')) {
cb(full);
}
}
}

function stabilizeDirectory(dir, nextVersion) {
const all = [];
walkWit(dir, (file) => {
for (const p of stabilizeFile(file, nextVersion)) {
all.push({ file, ...p });
}
});
return all;
}

if (require.main === module) {
const [dir, nextVersion] = process.argv.slice(2);
if (!dir || !nextVersion) {
console.error('Usage: node stabilize-features.js <wit-dir> <next-version>');
process.exit(1);
}
if (!fs.existsSync(dir)) {
console.error(`Directory not found: ${dir}`);
process.exit(1);
}

try {
const promoted = stabilizeDirectory(dir, nextVersion);
if (promoted.length === 0) {
console.log(`No @stabilize-next markers in ${dir}`);
} else {
for (const p of promoted) {
const rel = path.relative(process.cwd(), p.file);
console.log(`Stabilized feature '${p.feature}' -> @since(version = ${nextVersion}) (${rel})`);
}
}
} catch (err) {
console.error(`::error::${err.message}`);
process.exit(1);
}
}

module.exports = { stabilizeFile, stabilizeDirectory };
22 changes: 22 additions & 0 deletions .github/scripts/validate-since.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const DECLARATION_PATTERNS = [
const SINCE_PATTERN = /@since\s*\(\s*version\s*=\s*[0-9a-z.\-]+\s*\)/i;
const UNSTABLE_PATTERN = /@unstable\s*\(\s*feature\s*=\s*[a-z][a-z0-9-]*\s*\)/i;

/**
* Marker promoted by .github/scripts/stabilize-features.js during a release.
* It must sit immediately above an @unstable(feature = ...) gate; the release
* rewrites that gate to @since(version = NEXT) and drops the marker. A marker
* that survives (no gate below it) means a stabilization was left half-applied.
*/
const STABILIZE_MARKER_PATTERN = /^\s*\/\/\s*@stabilize-next\s*$/;

/**
* Check if a line has a preceding @since or @unstable annotation.
* Looks backward through lines, skipping doc comments (///).
Expand Down Expand Up @@ -81,6 +89,20 @@ function validateFile(filePath) {
for (let i = 0; i < lines.length; i++) {
const line = lines[i];

// A @stabilize-next marker must be immediately followed by an @unstable gate.
if (STABILIZE_MARKER_PATTERN.test(line)) {
const next = lines[i + 1] || '';
if (!UNSTABLE_PATTERN.test(next.trim())) {
errors.push({
file: filePath,
line: i + 1,
declaration: 'marker',
name: '@stabilize-next',
message: `'// @stabilize-next' must be immediately followed by an @unstable(feature = ...) gate`,
});
}
}

for (const { name, regex } of DECLARATION_PATTERNS) {
const match = line.match(regex);
if (match) {
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ jobs:
fi
done

- name: Stabilize gated features
run: |
WIT_DIR="${{ needs.prepare-release.outputs.wit_dir }}"
NEXT="${{ needs.prepare-release.outputs.next_version }}"

# Promote any feature gate marked `// @stabilize-next` to
# @since(version = NEXT). This runs after the package version is
# bumped to NEXT, which is the point at which @since(NEXT) is legal.
for proposal in $PROPOSALS; do
wit_path="proposals/$proposal/$WIT_DIR"
if [ -d "$wit_path" ]; then
node .github/scripts/stabilize-features.js "$wit_path" "$NEXT"
fi
done

- name: Update wit-deps
run: |
WIT_DIR="${{ needs.prepare-release.outputs.wit_dir }}"
Expand Down
1 change: 1 addition & 0 deletions proposals/cli/wit/exit.wit
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface exit {
///
/// This function does not return; the effect is analogous to a trap, but
/// without the connotation that something bad has happened.
// @stabilize-next
@unstable(feature = cli-exit-with-code)
exit-with-code: func(status-code: u8);
}
Loading