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
27 changes: 24 additions & 3 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name: Live smoke
#
# Required repo secrets:
# STRIPE_API_KEY — a Stripe secret key for the account whose Stripe Project has
# the providers linked (`stripe projects link vercel|render`,
# the providers linked (`stripe projects link vercel|render|cloudflare`,
# a one-time human step — provider links are account-level).
# VERCEL_TOKEN, RENDER_API_KEY — provider API creds (Vercel is also resolved from
# the Stripe-managed instance env, but the var is harmless).
Expand All @@ -32,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
provider: [vercel, render]
provider: [vercel, render, cloudflare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -63,7 +63,28 @@ jobs:
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" \
| sudo tee /etc/apt/sources.list.d/stripe.list
sudo apt-get update && sudo apt-get install -y stripe
stripe plugins install projects
stripe plugin install "projects@$(tr -d '[:space:]' < crates/stackless-stripe-projects/tests/fixtures/plugin-version.txt)"

# Validate the committed Stripe Projects snapshots against the live PINNED
# plugin before spending on cloud resources. `stripe-refresh` re-blesses
# and refuses to write if the live catalog has unmodeled drift, so this
# also gates catalog model coverage. Only the plugin-pinned artifacts must
# match exactly — catalog.json content is server-side and drifts on
# Stripe's schedule (the nightly watcher PRs those changes).
- name: Stripe Projects snapshot gate (pinned)
# No creds: `--version`, the `--help` surface, and `catalog --json` are
# all unauthenticated (the catalog is a public provider list).
run: |
pinned=$(tr -d '[:space:]' < crates/stackless-stripe-projects/tests/fixtures/plugin-version.txt)
installed=$(stripe projects --version | tr -d '[:space:]')
if [ "$installed" != "$pinned" ]; then
echo "::error::installed projects plugin $installed != pinned $pinned"; exit 1
fi
mise run stripe-refresh
git diff --exit-code -- \
crates/stackless-stripe-projects/tests/fixtures/command-surface.txt \
crates/stackless-stripe-projects/tests/fixtures/plugin-version.txt \
|| { echo "::error::command surface is stale for plugin $pinned — run 'mise run stripe-refresh' and commit"; exit 1; }

- name: Live ${{ matrix.provider }} up/down
env:
Expand Down
124 changes: 124 additions & 0 deletions .github/workflows/stripe-projects-watch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Stripe Projects watch

# The "never remember to upgrade" tier. Nightly, it installs the LATEST
# `stripe projects` plugin (ignoring the committed pin), re-blesses the
# snapshots, and — if anything changed (new plugin version, new commands/flags,
# new services, or unmodeled catalog drift) — opens/updates a PR carrying the
# fixture diff as the changelog Stripe never publishes. The only human action is
# reviewing that PR. Detection of new versions is automatic.
#
# The bless needs NO Stripe creds — `--version`, the `--help` surface, and
# `catalog --json` are all unauthenticated.
#
# Required repo secrets:
# GH_PAT — a PAT with repo scope, so the opened PR re-triggers ci.yml's gates
# (PRs created with the default GITHUB_TOKEN do not). Falls back to
# GITHUB_TOKEN if absent (PR opens, no CI).

on:
workflow_dispatch:
schedule:
- cron: "0 6 * * *" # nightly 06:00 UTC (ahead of the smoke run)

permissions:
contents: write
pull-requests: write

jobs:
watch:
name: watch stripe projects plugin
if: ${{ github.repository_owner == 'snowmead' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Install the mise CLI only, then install tools serially with a force-retry
# to avoid mise's parallel rustup component races on a cold cache.
- name: Install mise
uses: jdx/mise-action@v2
with:
install: false
cache: false
- name: Install tools (serial, retried)
env:
MISE_JOBS: "1"
shell: bash
run: |
mise install && exit 0
for i in 1 2; do echo "retry $i (force)"; sleep 10; mise install --force && exit 0; done
exit 1

- name: Rust / Cargo cache
uses: Swatinem/rust-cache@v2

- name: Install Stripe CLI + LATEST projects plugin
run: |
curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public \
| gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" \
| sudo tee /etc/apt/sources.list.d/stripe.list
sudo apt-get update && sudo apt-get install -y stripe
stripe plugin install projects

- name: Re-bless against the latest plugin
id: refresh
run: |
set +e
fixtures=crates/stackless-stripe-projects/tests/fixtures
old=$(tr -d '[:space:]' < "$fixtures/plugin-version.txt")
latest=$(stripe projects --version | tr -d '[:space:]')
mise run stripe-refresh >refresh.log 2>&1
status=$?
cat refresh.log
# The bless aborts before writing if the live catalog has unmodeled
# drift; bump the version file ourselves so the PR still carries the
# signal and the diff explains the model needs a new variant/field.
if [ "$status" -ne 0 ]; then
printf '%s\n' "$latest" > "$fixtures/plugin-version.txt"
fi
{
echo "old=$old"
echo "latest=$latest"
echo "bless_status=$status"
} >> "$GITHUB_OUTPUT"

- name: Compose PR body
env:
OLD: ${{ steps.refresh.outputs.old }}
LATEST: ${{ steps.refresh.outputs.latest }}
BLESS_STATUS: ${{ steps.refresh.outputs.bless_status }}
run: |
{
echo "Automated by \`.github/workflows/stripe-projects-watch.yml\`."
echo
echo "- Pinned plugin: \`$OLD\`"
echo "- Latest plugin: \`$LATEST\`"
echo
if [ "$BLESS_STATUS" -ne 0 ]; then
echo "> ⚠️ **The re-bless failed** — the live catalog has wire-format the typed"
echo "> model does not cover. Update \`crates/stackless-stripe-projects/src/catalog.rs\`,"
echo "> then run \`mise run stripe-refresh\` to complete the snapshots."
echo
echo '```'
tail -n 40 refresh.log
echo '```'
else
echo "Re-blessed cleanly. Review the fixture diff below as the changelog:"
echo "command-surface.txt (commands/flags), catalog.json (services/schemas/pricing)."
fi
} > pr-body.md

- name: Open / update upgrade PR
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
branch: bot/stripe-projects-update
base: main
commit-message: "chore(stripe-projects): plugin ${{ steps.refresh.outputs.old }} → ${{ steps.refresh.outputs.latest }}"
title: "chore(stripe-projects): plugin ${{ steps.refresh.outputs.old }} → ${{ steps.refresh.outputs.latest }}"
body-path: pr-body.md
add-paths: |
crates/stackless-stripe-projects/tests/fixtures/command-surface.txt
crates/stackless-stripe-projects/tests/fixtures/plugin-version.txt
crates/stackless-stripe-projects/tests/fixtures/catalog.json
delete-branch: true
41 changes: 41 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Run by `prek` (the Rust pre-commit runner, installed via mise). Hooks are
# auto-wired by mise.toml's [hooks].postinstall on `mise install`; re-wire by
# hand with `mise run hooks`. Every hook shells out to a `mise run` task so the
# toolchain matches CI exactly.
#
# pre-commit: fast, offline checks that must never block a commit for long.
# pre-push: the full local correctness gate (no network — supply-chain stays
# a CI-only job).
repos:
- repo: local
hooks:
- id: fmt
name: cargo fmt --check
entry: mise run fmt
language: system
pass_filenames: false
stages: [pre-commit]
- id: taplo
name: taplo fmt --check
entry: mise run taplo
language: system
pass_filenames: false
stages: [pre-commit]
- id: stripe-coherence
name: stripe projects fixtures coherent
entry: mise run stripe-coherence
language: system
pass_filenames: false
stages: [pre-commit]
- id: check
name: fmt + clippy + taplo
entry: mise run check
language: system
pass_filenames: false
stages: [pre-push]
- id: test
name: cargo nextest (workspace, all features)
entry: mise run test
language: system
pass_filenames: false
stages: [pre-push]
9 changes: 7 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,13 @@ proven there.
`import` commands (`init --from` exists but nothing can generate a
share URL yet — the same gap atto's `config.ts` recorded). Until the
new flags land, the backend keeps `cloud-env.ts`'s plain-mode
fallbacks for `--json`'s confirmation/auth quirks; re-test on each
plugin release. Environment membership (`env add`/`env remove`) can
fallbacks for `--json`'s confirmation/auth quirks. The pinned
version of record and the full command surface are committed
snapshots
(`crates/stackless-stripe-projects/tests/fixtures/{plugin-version,command-surface}.txt`,
plus `catalog.json`); CI re-tests them against each release and a
nightly watcher opens an upgrade PR automatically (see
`docs/SELFTEST.md`). Environment membership (`env add`/`env remove`) can
express a resource shared across instances — the seam for
invariant 8's "unless the definition explicitly says so" (not in
the v0 schema).
Expand Down
32 changes: 32 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# stackless

Disposable software stacks: named, leased, isolated, proven, accounted for,
destroyed. A Rust workspace (edition 2024); `crates/stackless` is the CLI.

## Project docs — read the relevant one before working in that area

- **[ARCHITECTURE.md](ARCHITECTURE.md)** — the architecture in numbered sections;
code comments cite it as `§N`. Read before touching core / engine / the
`Substrate` & integration seams.
- **[VISION.md](VISION.md)** — founding vision: what stackless is and why.
- **[README.md](README.md)** — overview + quickstart.
- **[docs/SCHEMA.md](docs/SCHEMA.md)** — the complete `stackless.toml` schema
reference (what the parser/validator actually enforce).
- **[docs/SELFTEST.md](docs/SELFTEST.md)** — the two-tier testing strategy
(hermetic + gated live smoke) and the Stripe Projects plugin snapshot/drift
framework. Read before changing tests or the smoke setup.
- **[docs/ADDING-A-PROVIDER.md](docs/ADDING-A-PROVIDER.md)** — how to add a
hosting substrate or a catalog integration: the one-row registration seams,
the onboarding tooling below, and the hard-won gotchas. Read before adding or
changing any provider.

## Provider-onboarding tooling (the `xtask` crate; see ADDING-A-PROVIDER.md)

- `mise run catalog <provider>` — list a provider's catalog services + config
schemas + pricing (offline).
- `mise run discover <reference> -- --dir <project-dir>` — provision a resource
once into a throwaway environment, dump its real credential output env vars,
then tear down (live; needs `STRIPE_API_KEY` + a linked project). The catalog
describes config *input* but not credential *outputs* — this pins them.
- `mise run new-integration <reference>` — scaffold a provider module from the
catalog schema.
21 changes: 21 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "3"
members = [
"crates/stackless-core",
"crates/stackless-cloud",
"crates/stackless-daemon",
"crates/stackless-integrations",
"crates/stackless-local",
Expand All @@ -12,6 +13,7 @@ members = [
"crates/stackless",
"crates/render-client",
"crates/vercel-client",
"crates/xtask",
]

[workspace.package]
Expand All @@ -32,6 +34,7 @@ dbg_macro = "deny"

[workspace.dependencies]
stackless-core = { path = "crates/stackless-core" }
stackless-cloud = { path = "crates/stackless-cloud" }
stackless-daemon = { path = "crates/stackless-daemon" }
stackless-local = { path = "crates/stackless-local" }
stackless-git = { path = "crates/stackless-git" }
Expand Down
17 changes: 17 additions & 0 deletions crates/stackless-cloud/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "stackless-cloud"
edition.workspace = true
version.workspace = true
license.workspace = true
repository.workspace = true

[lints]
workspace = true

[dependencies]
stackless-core.workspace = true
stackless-git.workspace = true
tempfile = "3.27.0"

[dev-dependencies]
tempfile = "3.27.0"
Loading
Loading