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: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Keep source, policy fixtures, and generated evidence byte-stable on every OS.
* text=auto eol=lf
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,80 @@ jobs:
run: make vuln
- name: Dependency footprint report
run: make dependency-report
platform-core:
name: platform-core (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
env:
GOTOOLCHAIN: local
strategy:
fail-fast: false
matrix:
include:
- platform: linux-amd64
runner: ubuntu-24.04
goos: linux
goarch: amd64
- platform: linux-arm64
runner: ubuntu-24.04-arm
goos: linux
goarch: arm64
- platform: macos-arm64
runner: macos-15
goos: darwin
goarch: arm64
- platform: windows-amd64
runner: windows-2022
goos: windows
goarch: amd64
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # actions/checkout v7.0.0
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # actions/setup-go v6.4.0
with:
go-version: 1.26.x
check-latest: true
- name: Verify runner platform
shell: pwsh
run: |
$actualOS = go env GOOS
$actualArch = go env GOARCH
if ($actualOS -ne "${{ matrix.goos }}" -or $actualArch -ne "${{ matrix.goarch }}") {
throw "unexpected Go runner platform: $actualOS/$actualArch"
}
- name: Root module verification
env:
GOWORK: off
run: go mod verify
- name: Root module build
run: go build ./...
- name: Root module tests
run: go test ./...
- name: Root examples
run: go test ./... -run '^Example'
- name: Generate portable service fixture
id: fixture
shell: pwsh
working-directory: contrib
run: |
$serviceDir = Join-Path $env:RUNNER_TEMP "api-toolkit-platform-${{ matrix.platform }}"
$contribDir = Join-Path $env:GITHUB_WORKSPACE "contrib"
go run ./cmd/api-toolkit new service `
--module example.com/platform-smoke `
--profile saas-api `
--dir "$serviceDir" `
--core-replace "$env:GITHUB_WORKSPACE" `
--contrib-replace "$contribDir"
"directory=$serviceDir" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Generated service dependency resolution
working-directory: ${{ steps.fixture.outputs.directory }}
env:
GOWORK: off
run: go mod tidy
- name: Generated service build
working-directory: ${{ steps.fixture.outputs.directory }}
env:
GOWORK: off
run: go build ./...
postgres-contract:
runs-on: ubuntu-latest
env:
Expand Down
28 changes: 22 additions & 6 deletions contrib/cmd/api-toolkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"runtime/debug"
Expand Down Expand Up @@ -1035,9 +1036,9 @@ func writeGeneratedFileReplace(root *os.Root, name string, data []byte) error {
if root == nil {
return errors.New("output root is required")
}
clean := filepath.Clean(name)
if clean != name || filepath.IsAbs(clean) || strings.HasPrefix(clean, ".."+string(filepath.Separator)) || clean == ".." {
return fmt.Errorf("unsafe generated path %q", name)
clean, err := portableGeneratedPath(name)
if err != nil {
return err
}
if parent := filepath.Dir(clean); parent != "." {
if err := root.MkdirAll(parent, 0o750); err != nil {
Expand Down Expand Up @@ -4911,9 +4912,9 @@ func writeGeneratedFile(root *os.Root, name string, data []byte) error {
if root == nil {
return errors.New("output root is required")
}
clean := filepath.Clean(name)
if clean != name || filepath.IsAbs(clean) || strings.HasPrefix(clean, ".."+string(filepath.Separator)) || clean == ".." {
return fmt.Errorf("unsafe generated path %q", name)
clean, err := portableGeneratedPath(name)
if err != nil {
return err
}
if parent := filepath.Dir(clean); parent != "." {
if err := root.MkdirAll(parent, 0o750); err != nil {
Expand All @@ -4931,6 +4932,21 @@ func writeGeneratedFile(root *os.Root, name string, data []byte) error {
return nil
}

func portableGeneratedPath(name string) (string, error) {
if name == "" || strings.ContainsRune(name, '\x00') || strings.ContainsAny(name, `\:`) {
return "", fmt.Errorf("unsafe generated path %q", name)
}
clean := path.Clean(name)
if clean != name || clean == "." || path.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, "../") {
return "", fmt.Errorf("unsafe generated path %q", name)
}
native := filepath.FromSlash(clean)
if filepath.IsAbs(native) || filepath.VolumeName(native) != "" {
return "", fmt.Errorf("unsafe generated path %q", name)
}
return native, nil
}

func renderTemplate(name, body string, data map[string]string) ([]byte, error) {
tmpl, err := template.New(name).Parse(body)
if err != nil {
Expand Down
89 changes: 89 additions & 0 deletions contrib/cmd/api-toolkit/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,95 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}

func TestPortableGeneratedPath(t *testing.T) {
for _, name := range []string{
"client.go",
".github/workflows/ci.yml",
"internal/httpapi/server.go",
} {
t.Run(name, func(t *testing.T) {
got, err := portableGeneratedPath(name)
if err != nil {
t.Fatalf("portableGeneratedPath(%q): %v", name, err)
}
if want := filepath.FromSlash(name); got != want {
t.Fatalf("portableGeneratedPath(%q) = %q, want %q", name, got, want)
}
})
}

for _, name := range []string{
"",
".",
"..",
"../outside.go",
"nested/../../outside.go",
"/absolute.go",
"//server/share.go",
"C:/outside.go",
`C:\outside.go`,
`nested\file.go`,
"nested:stream.go",
"nested//file.go",
"nested/./file.go",
"nested/../file.go",
"nested/file.go/",
"nested/\x00file.go",
} {
t.Run("reject_"+strconv.Quote(name), func(t *testing.T) {
if _, err := portableGeneratedPath(name); err == nil {
t.Fatalf("portableGeneratedPath(%q) succeeded, want rejection", name)
}
})
}
}

func TestScaffoldManifestPathsArePortable(t *testing.T) {
manifests := []struct {
name string
files []scaffoldFile
}{
{"default", scaffoldFiles},
{"full", fullScaffoldFiles},
{"typescript", fullTypeScriptClientScaffoldFiles},
{"provider-replay", providerReplayScaffoldFiles},
{"saas-web", saasWebScaffoldFiles},
{"stripe", stripeBillingScaffoldFiles},
{"resend", resendEmailScaffoldFiles},
{"clerk", clerkWebhooksScaffoldFiles},
{"entitlements", entitlementsScaffoldFiles},
}
for _, manifest := range manifests {
t.Run(manifest.name, func(t *testing.T) {
for _, file := range manifest.files {
if _, err := portableGeneratedPath(file.Name); err != nil {
t.Errorf("manifest path %q: %v", file.Name, err)
}
}
})
}
}

func TestWriteGeneratedFileRejectsTraversalBeforeWriting(t *testing.T) {
parent := t.TempDir()
output := filepath.Join(parent, "output")
if err := os.Mkdir(output, 0o750); err != nil {
t.Fatalf("create output: %v", err)
}
root, err := os.OpenRoot(output)
if err != nil {
t.Fatalf("open output root: %v", err)
}
defer root.Close()

if err := writeGeneratedFile(root, "../outside.go", []byte("escaped")); err == nil {
t.Fatal("writeGeneratedFile traversal succeeded, want rejection")
}
if _, err := os.Stat(filepath.Join(parent, "outside.go")); !os.IsNotExist(err) {
t.Fatalf("traversal created an outside file: %v", err)
}
}

func TestRunVersion(t *testing.T) {
var out strings.Builder
code := run(context.Background(), []string{"version"}, &out, &out)
Expand Down
14 changes: 14 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ source of truth is `docs/release-runbook.md`.

## 2026-08-22

### Cross-platform core verification

- Root-module verification, builds, tests, examples, and a generated
`saas-api` service build now gate Linux amd64, Linux arm64, macOS arm64, and
Windows amd64 pull requests on fixed GitHub-hosted runner labels.
- Repository-owned text is normalized to LF on every checkout, and generated
service dependencies are resolved before the isolated build gate runs.
- The generator now validates canonical slash-form manifest paths before
converting them to host separators, allowing nested templates on Windows
without weakening rooted traversal protection.
- Full contrib and race verification remain Linux amd64 gates. The support
policy does not claim macOS amd64 or Windows arm64 without matching required
workflow evidence.

### Real Redis contract foundation

- `make test-redis` now provides an isolated Redis 7 harness and real-service
Expand Down
2 changes: 1 addition & 1 deletion docs/site/search-index.json

Large diffs are not rendered by default.

34 changes: 26 additions & 8 deletions docs/support-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,40 @@ older-major API baseline before publication evidence is produced.

## Platform

The required CI platform is:
The required root-module CI platforms are:

| OS | Architecture | Status | Evidence |
| --- | --- | --- | --- |
| Linux | amd64 | Supported | GitHub-hosted Ubuntu CI runs unit, race, vuln, lint, docs, API, and fuzz smoke gates. |
| Linux | amd64 | Supported | `platform-core (linux-amd64)` on `ubuntu-24.04` runs root verification, build, tests, examples, and a generated-service build; the Linux quality workflow also runs race, vulnerability, lint, docs, API, and fuzz gates. |
| Linux | arm64 | Supported | `platform-core (linux-arm64)` on `ubuntu-24.04-arm` runs root verification, build, tests, examples, and a generated-service build. |
| macOS | arm64 | Supported | `platform-core (macos-arm64)` on `macos-15` runs root verification, build, tests, examples, and a generated-service build. |
| Windows | amd64 | Supported | `platform-core (windows-amd64)` on `windows-2022` runs root verification, build, tests, examples, and a generated-service build. |

Other platforms are portability goals, not supported release gates:
Root and generated-service compilation are required on every supported platform
with current tested Go (`1.26.x`). Race testing remains a Linux amd64 gate.
Contrib remains Linux-only for full unit and integration verification; the
portable generated service is the cross-platform CLI evidence.

Git normalizes repository-owned text files to LF through `.gitattributes` so
byte-sensitive fixtures and policy manifests have identical content on Linux,
macOS, and Windows checkouts. Generated applications remain free to choose
their own line-ending policy after generation.

Generator manifests use canonical slash-form relative paths. The CLI rejects
absolute, traversal, backslash, volume/alternate-stream, and NUL-bearing names
before mapping a validated manifest path to the host separator and writing it
through a rooted filesystem handle.

The following pairs are portability goals, not supported release targets:

| OS | Architecture | Status | Notes |
| --- | --- | --- | --- |
| macOS | amd64/arm64 | Best effort | Expected for pure Go packages, but not a required release gate. |
| Windows | amd64/arm64 | Best effort | Expected for pure Go packages that avoid Unix assumptions, but not a required release gate. |
| Linux | arm64 | Best effort | Expected for pure Go packages, but generated deployment assets are not release-gated on arm64. |
| macOS | amd64 | Not supported | No required platform workflow. |
| Windows | arm64 | Not supported | No required platform workflow. |

Do not claim broad OS/architecture support in README, release notes, or package
docs until CI includes matching smoke checks.
Do not claim broader OS/architecture support in README, release notes, or
package docs without adding matching required CI checks and updating this
policy.

## PostgreSQL Adapter Test Support

Expand Down
53 changes: 52 additions & 1 deletion docscheck/contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,57 @@ func TestToolchainPolicyMatchesModulesAndWorkflows(t *testing.T) {
}
}

func TestPlatformSupportPolicyMatchesWorkflow(t *testing.T) {
repoRoot := mustRepoRoot(t)
attributes := readText(t, filepath.Join(repoRoot, ".gitattributes"))
if !strings.Contains(attributes, "* text=auto eol=lf") {
t.Fatal(".gitattributes must normalize repository-owned text to LF on every runner")
}

ci := readText(t, filepath.Join(repoRoot, ".github", "workflows", "ci.yml"))
for _, required := range []string{
"platform-core:",
"platform-core (${{ matrix.platform }})",
"platform: linux-amd64\n runner: ubuntu-24.04\n goos: linux\n goarch: amd64",
"platform: linux-arm64\n runner: ubuntu-24.04-arm\n goos: linux\n goarch: arm64",
"platform: macos-arm64\n runner: macos-15\n goos: darwin\n goarch: arm64",
"platform: windows-amd64\n runner: windows-2022\n goos: windows\n goarch: amd64",
"Verify runner platform",
"GOWORK: off",
"GOTOOLCHAIN: local",
"Root module verification\n env:\n GOWORK: off\n run: go mod verify",
"go build ./...",
"go test ./...",
"go test ./... -run '^Example'",
"Generate portable service fixture",
"shell: pwsh",
"Generated service dependency resolution",
"run: go mod tidy",
"Generated service build",
"contents: read",
} {
if !strings.Contains(ci, required) {
t.Fatalf(".github/workflows/ci.yml missing platform verification requirement %q", required)
}
}

support := readText(t, filepath.Join(repoRoot, "docs", "support-policy.md"))
for _, required := range []string{
"| Linux | amd64 | Supported |",
"| Linux | arm64 | Supported |",
"| macOS | arm64 | Supported |",
"| Windows | amd64 | Supported |",
"Root and generated-service compilation",
"Contrib remains Linux-only",
"Git normalizes repository-owned text files to LF",
"Generator manifests use canonical slash-form relative paths",
} {
if !strings.Contains(support, required) {
t.Fatalf("docs/support-policy.md missing platform support policy %q", required)
}
}
}

func TestProductionCodeUsesCheckedResponseWriters(t *testing.T) {
repoRoot := mustRepoRoot(t)
allowed := func(rel string) bool {
Expand Down Expand Up @@ -7936,7 +7987,7 @@ func TestQualityAuditP1DependencyWorthinessDocs(t *testing.T) {
"| Linux | amd64 | Supported |",
"macOS",
"Windows",
"Do not claim broad OS/architecture support",
"Do not claim broader OS/architecture support",
} {
if !strings.Contains(support, required) {
t.Fatalf("docs/support-policy.md missing %q", required)
Expand Down
Loading