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 .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
set -eu
cd "$(git rev-parse --show-toplevel)"
bun scripts/check-publication.ts --message-file "$1"
5 changes: 5 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
cd "$(git rev-parse --show-toplevel)"
bun scripts/check-publication.ts --staged
gitleaks git --pre-commit --staged --no-banner --redact --config .gitleaks.toml .
19 changes: 19 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh
set -eu
cd "$(git rev-parse --show-toplevel)"
bun scripts/check-publication.ts
while read -r local_ref local_sha remote_ref remote_sha; do
[ "$local_sha" != 0000000000000000000000000000000000000000 ] || continue
if [ "$remote_sha" = 0000000000000000000000000000000000000000 ]; then
if git rev-parse --verify --quiet origin/dev >/dev/null; then
base=$(git merge-base origin/dev "$local_sha")
range="$base..$local_sha"
else
range="$local_sha"
fi
else
range="$remote_sha..$local_sha"
fi
bun scripts/check-publication.ts --range "$range"
gitleaks git --no-banner --redact --config .gitleaks.toml --log-opts="$range" .
done
7 changes: 7 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Change

Describe the problem and resulting behavior.

## Validation

Commands run and relevant results. State any missing proof.
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ jobs:
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.0
- name: Validate public contribution text
env:
PUBLIC_TITLE: ${{ github.event.pull_request.title }}
PUBLIC_BODY: ${{ github.event.pull_request.body }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: bun scripts/check-publication.ts --metadata-env --range "$BASE_SHA..HEAD"

- name: Install verified Gitleaks
env:
GITLEAKS_ARCHIVE: gitleaks_8.30.1_linux_x64.tar.gz
GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
GITLEAKS_VERSION: 8.30.1
run: |
set -euo pipefail
curl --fail --silent --show-error --location \
--output "$RUNNER_TEMP/$GITLEAKS_ARCHIVE" \
"https://github.com/gitleaks/gitleaks/releases/download/v$GITLEAKS_VERSION/$GITLEAKS_ARCHIVE"
printf '%s %s\n' "$GITLEAKS_SHA256" "$RUNNER_TEMP/$GITLEAKS_ARCHIVE" | sha256sum --check --strict
tar --extract --gzip --file "$RUNNER_TEMP/$GITLEAKS_ARCHIVE" --directory "$RUNNER_TEMP" gitleaks
printf '%s\n' "$RUNNER_TEMP" >> "$GITHUB_PATH"

- name: Prepare pinned Rust toolchain
run: rustup toolchain install 1.90 --profile minimal --component rustfmt --target wasm32-wasip2
- run: bun install --frozen-lockfile
Expand Down
4 changes: 4 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
title = "Rootform editor secret scanning"

[extend]
useDefault = true
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ Read `docs/editor-contract.md` and the affected editor package before editing.

Local development evidence stays outside the public repository. Do not commit
private implementation, project paths, inputs, credentials or operator notes.

Configure `git config core.hooksPath .githooks` before contributing. Validate
public messages before sending; diagnostics never reproduce sensitive values.
62 changes: 23 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,37 @@
# Rootform editors

VS Code and Zed extensions for Rootform `.rf.hcl` source, powered by
`rootform lsp` from a local Rootform CLI. The extensions do not include or
download Rootform; language semantics come from Rootform's compiler, and the
clients own syntax presentation, launch and lifecycle.
VS Code and Zed extensions for `.rf.hcl` source: diagnostics, completion,
hover, definitions and formatting through `rootform lsp`.

- [VS Code](vscode/README.md), for the Visual Studio Marketplace and Open VSX
- [Zed](zed/README.md), for the Zed extension registry
Install a local [Rootform CLI](https://docs.rootform.dev/installation/), then
choose your editor:

## Development
| Editor | Setup |
| --- | --- |
| VS Code (Visual Studio Marketplace, Open VSX) | [Extension guide](vscode/README.md) |
| Zed (extension registry) | [Extension guide](zed/README.md) |

```sh
bun install --frozen-lockfile
bun run verify
```

`verify` checks that both manifests share one version and description, checks
formatting, builds and unit-tests the VS Code client, tests the Zed queries
against the pinned HCL grammar and builds the Zed package for `wasm32-wasip2`
with Rust 1.90, the toolchain the Zed registry packages with.
Both clients launch an existing executable from settings or `PATH`. They ship
no compiler, download no Rootform binary and execute no providers.
See [editing Rootform source](https://docs.rootform.dev/language/editors/).

Real integration qualification uses an explicitly provided Rootform executable
and a local VS Code. Nothing is downloaded:
## Develop

```sh
VSCODE_EXECUTABLE_PATH="/Applications/Visual Studio Code.app/Contents/MacOS/Code" \
ROOTFORM_BINARY="/absolute/path/to/rootform" \
bun run --cwd vscode test:integration
bun install --frozen-lockfile
bun run verify
```

The runner uses a temporary multi-root fixture workspace with unique VS Code
user-data and extension directories. The fixture source is synthetic.

To try the Zed package, run `zed: install dev extension` in Zed and select the
`zed/` directory. Zed may need network access to prepare the pinned HCL
grammar and `wasi-sdk` while building it; the language server always comes
from the local Rootform executable.

Native editor runtime qualification currently covers macOS arm64. The Zed
package also builds for `wasm32-wasip2`; Linux and Windows editor runtimes
have not yet been qualified.

## Releasing
The gate validates manifests, formatting, VS Code build and tests, Zed grammar
queries and the `wasm32-wasip2` package with Rust 1.90. Real editor integration
requires an explicitly provided local Rootform executable; see the
[editor contract](docs/editor-contract.md).
Native editor qualification covers macOS arm64. Linux and Windows runtime
qualification remains separate from package checks.

[Releasing](docs/releasing.md) covers versioning, the release workflow and
publication to each store.
[Releasing](docs/releasing.md) covers versioning and store publication.

## License

Source is [Apache-2.0](LICENSE). Third-party dependencies retain their original
licenses. Rootform name and logo: trademark rights reserved, see
[TRADEMARKS.md](https://github.com/rootform-dev/rootform/blob/dev/TRADEMARKS.md).
[Apache-2.0](LICENSE). Third-party dependencies retain their licenses.
[Rootform trademark terms](https://github.com/rootform-dev/rootform/blob/dev/TRADEMARKS.md) apply.
118 changes: 118 additions & 0 deletions scripts/check-publication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env bun
import { execFileSync } from "node:child_process";
import { lstatSync, readFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { assertPublicMessage, publicationIssues } from "./publication-safety.ts";

export function trackedPublicationIssues(
directory: string,
revision?: string,
): Array<{ path: string; rule: string; line: number }> {
const git = (args: string[]) => {
try {
return execFileSync("git", args, {
cwd: directory,
maxBuffer: 64 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
});
} catch {
throw new Error("Publication refused: cannot read tracked Git content");
}
};
const records = git(
revision && revision !== ":" ? ["ls-tree", "-r", "-z", revision] : ["ls-files", "-s", "-z"],
)
.toString()
.split("\0")
.filter(Boolean);
const entries = new Map(
records.map((entry) => {
const separator = entry.indexOf("\t");
return [entry.slice(separator + 1), entry.slice(0, separator).split(" ")[0]] as const;
}),
);
return [...entries].flatMap(([path, mode]) => {
const full = join(directory, path);
let body: Buffer;
if (revision) {
body = git(["show", `${revision === ":" ? "" : revision}:${path}`]);
if (mode === "120000") {
return [{ path, rule: "symlink", line: 1 }];
}
if (mode !== "100644" && mode !== "100755")
return [{ path, rule: "irregular-entry", line: 1 }];
} else {
let stat: ReturnType<typeof lstatSync>;
try {
stat = lstatSync(full);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
throw new Error("Publication refused: cannot inspect tracked entry");
}
if (stat.isSymbolicLink()) {
return [{ path, rule: "symlink", line: 1 }];
}
if (!stat.isFile()) return [{ path, rule: "irregular-entry", line: 1 }];
try {
body = readFileSync(full);
} catch {
throw new Error("Publication refused: cannot read tracked file");
}
}
// Binary metadata is included; a tracked path is never skipped because it is ignored.
return publicationIssues(body.toString("utf8")).map((issue) => ({ path, ...issue }));
});
}

export function checkPublication(args: string[]): void {
const option = (name: string) => {
const index = args.indexOf(name);
return index < 0 ? undefined : args[index + 1];
};
const directory = resolve(option("--repo") ?? process.cwd());
const git = (args: string[]) => {
try {
return execFileSync("git", args, {
cwd: directory,
maxBuffer: 64 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
}).toString();
} catch {
throw new Error("Publication refused: cannot inspect Git history");
}
};
const message = option("--message-file");
if (message) assertPublicMessage(readFileSync(message, "utf8"));
if (args.includes("--metadata-env"))
assertPublicMessage([process.env.PUBLIC_TITLE ?? "", process.env.PUBLIC_BODY ?? ""]);
const issues = message
? []
: trackedPublicationIssues(directory, args.includes("--staged") ? ":" : undefined);
const range = option("--range");
if (range) {
for (const commit of git(["rev-list", range]).trim().split("\n").filter(Boolean)) {
assertPublicMessage(git(["show", "-s", "--format=%B", commit]));
issues.push(...trackedPublicationIssues(directory, commit));
}
}
const issue = issues[0];
if (issue) {
// A malicious filename can itself contain sensitive text.
const location = publicationIssues(issue.path).length ? "tracked file" : issue.path;
throw new Error(`Publication refused: ${issue.rule} (${location}:${issue.line})`);
}
}

if (import.meta.main) {
try {
checkPublication(process.argv.slice(2));
} catch (error) {
const message = error instanceof Error ? error.message : "";
console.error(
/^(?:Publication|Public message) refused: /u.test(message)
? message
: "Publication refused: cannot inspect contribution",
);
process.exit(1);
}
}
Loading
Loading