* fix: keep running containers until stack pull/build succeeds
Acquire images before reconcile, capture a recovery generation for
compensation, and only remove classified orphans after handoff.
* fix: address recovery audit blockers for safe stack updates
Retire abandoned and expired recovery artifacts, probe compensated
runtimes before reporting rollback success, preserve local Docker when
deleting a node, validate the exact Compose invocation before capture,
and repair updateStack return-contract fixtures.
* fix: resolve ESLint errors blocking CI on this branch
Unused-import and unused-variable errors left over from the stack
deletion refactor: MeshService in stacks.ts (its opt-out cascade moved
into DeployedStackDeletionService), a redundant pruneVolumes
destructure in deleteDeployedStack (the real one is re-derived from
the same input object inside runDeletionBody), and an unused beforeAll
import in a Docker-integration test stub. Also scopes the webhook
pull-action case body in a block to satisfy no-case-declarations;
purely syntactic, no behavior change.
* fix: harden recovery probe, cleanup retry, and failed-pull Docker test
Reject absent or unhealthy expected replicas before reporting rollback
success, keep cleanup records until artifacts are actually removed, fail
closed when a mesh override cannot be generated, and assert a real
failed pull leaves the original container running.
* fix: verify recovery probe image identity and stack-scoped override paths
Reject recovered runtimes that use the wrong image or leave scale-zero
services running, and confine tombstone override deletion to the intent
stack directory so forged cross-stack paths cannot be swept.
* test: batch notification cap fixtures in a SQLite transaction
Unbatched 1200-row inserts were timing out at the default 30s under
CI load even though the same assertions pass in under 2s when green.
* feat: add routable browser URLs for stacks and shell views
Sync in-memory navigation to the address bar via a History API hook so
deep links, refresh, Back/Forward, and bookmarks work across nodes,
views, stack editor tabs, and mobile surfaces. Gate role/tier URL
normalization on permissions and license readiness, preserve URLs on
metadata fetch failure, and surface retryable stack-list errors without
rewriting pending stack paths.
* fix: preserve deep-link views on cold load and refresh
Stop the node-switch effect from resetting to dashboard on initial mount.
Defer URL writer settlement until hydrated activeView matches the route.
Adds E2E coverage for shell cold loads, stack refresh, and compose env tab.
* fix: keep mobile dashboard on list surface so sidebar renders
On mobile, the URL sync hook was routing /nodes/<slug>/dashboard to the
content surface, hiding the stack list sidebar. This prevented the
data-stacks-loaded sentinel from appearing, causing sidebar truncation
E2E tests to time out after reload on a mobile viewport.
Mobile dashboard now stays on the list surface; other non-editor views
still render on the content surface.
* fix: complete mobile URL routing follow-ups for stack deep links
Restore mobile /dashboard vs /stacks, list surface always writes /stacks.
Hydrate pendingDetailStack, freeze compose failures with routeDetailError,
and add unit plus E2E coverage.
* fix: hydrate shell views from URL and sync in-app navigation
Bootstrap activeView and tab state from the pathname on cold load.
Settle route phase when state already matches, normalize unknown segments,
and open Monaco editor tabs from stack deep links via applyEditorRouteState.
* fix: prevent mobile stack deep links from hanging on cold load
The resolvePendingStack effect did not re-fire when the pending stack ref
was populated during URL hydration, because the urlHydratingStack state
set in the same callback was not listed in the effect's dependency array.
Adding it causes the effect to retry once hydration has committed.
A resolvingRef mutex prevents concurrent invocations. When the target file
is already loaded, route state is applied directly without calling
loadFileForRoute, which avoids unmounting the editor (and hiding the
recovery chip) if a background refresh triggers route resolution during
a deploy operation.
* test: adapt stack, deploy, and sidebar e2e specs to routable stack URLs
* ci: raise E2E Playwright job timeout to 20 minutes
The sync-docs job mirrored /docs into the sencho-docs repo on every push
to main but was always skipped on PR events, surfacing as a permanent
'skipped' status check that did not reflect any real work. Removed
entirely; docs publishing will be wired up separately.
With no remaining job consuming the push trigger (the four CI jobs all
gate on github.event_name == 'pull_request'), the workflow now runs only
on pull_request events. This eliminates the empty workflow runs that
fired on every merge to main.
* ci: harden CI and supply-chain pipeline
* Add frontend Vitest step to ci.yml so the 241 existing frontend tests run on
every PR (mirrors the backend build/test/lint/audit order).
* Pin Node 26 as a single source of truth: new .node-version, node-version-file
on all setup-node calls, engines.node ">=26.0.0" in all three package.json
files. Matches the Dockerfile's node:26-alpine.
* SHA-pin remaining mutable actions in the start-app composite
(actions/setup-node v6, actions/cache v4.3.0).
* Pin Dockerfile supply-chain inputs: golang:1.26.3-alpine by sha256 digest in
both builder stages; replace mutable-tag git clone with commit-SHA fetch for
docker/cli (v29.4.1) and docker/compose (v5.1.3). LDFLAGS version strings
and otel patch preserved unchanged.
* Ref-scope docker-publish concurrency so two different release tags cannot
cancel each other; same-ref reruns still cancel as before.
* Harden CLA workflow: drop actions:write from permissions; tighten the
issue_comment trigger to PRs only (github.event.issue.pull_request != null)
matching the two documented CLA phrases. No PR code is checked out.
* Drop trivy-version: latest from both Trivy scans so the SHA-pinned
aquasecurity/trivy-action governs the bundled binary version. The
HIGH/CRITICAL gate, severity filter, and trivy.yaml (OpenVEX) are unchanged.
* Restructure Dependabot: add applies-to: security-updates groups for npm
(root/backend/frontend), docker, and github-actions; switch github-actions
to directories so the local composite action is monitored alongside the
top-level workflows.
* Add a daily scheduled SARIF security scan (security-scan.yml): two parallel
jobs scanning saelix/sencho:latest and a fresh main HEAD build, uploading to
GitHub code scanning. Least-privilege (contents: read, security-events:
write). Visibility only; existing PR-blocking and release-blocking Trivy
gates are not weakened.
Validation: backend tsc clean; frontend tsc clean; frontend npm test 27 files
/ 241 tests pass; npm audit --audit-level=high passes at root, backend, and
frontend; docker buildx build --check passes with no warnings (all pinned
digests resolve from the registry).
* ci(frontend): set explicit jsdom URL so localStorage initializes in CI
jsdom does not instantiate window.localStorage / sessionStorage when the
document has the opaque about:blank origin. Five frontend test files that
call localStorage.clear() in beforeEach started failing once the new
frontend Vitest step in this PR began running them on Linux CI runners.
Configuring environmentOptions.jsdom.url with a real same-origin URL is
the documented Vitest 4.x workaround and is a config-only change. All 27
test files (241 tests) pass locally with the fix applied.
The release-only `update-screenshots` job opened a `chore/refresh-screenshots`
PR and immediately tried to squash-merge it. Branch protection (1 review,
6 status checks) rejected the merge on every release, leaving an open PR
behind. Screenshots will instead be refreshed manually after UI changes.
Removed:
- The `update-screenshots` job from ci.yml (~57 lines).
- The `paths-ignore: docs/images/**` push trigger filter; its sole purpose
was to break the auto-merge re-trigger cascade. Its absence also fixes a
latent bug where docs-only pushes to main would have skipped sync-docs.
- Four `head_ref != 'chore/refresh-screenshots'` guards in other jobs.
- The "doc screenshots" mention in the skip-bot-PRs comment.
Reworked the screenshot capture spec to be opt-in:
- playwright.config.ts now defines two projects. The default `chromium`
project ignores screenshots.spec.ts; a separate `screenshots` project
matches it and is invoked manually.
- The e2e CI job runs `--project=chromium` so the screenshots project
cannot accidentally run in CI.
- Updated the spec's module comment with the new manual invocation.
Net: 86 lines removed, 27 added.
* fix(docker): build compose plugin from ./cmd, not ./cmd/compose
The compose-builder stage in Dockerfile was building ./cmd/compose, which
in docker/compose v5.1.2 is `package compose` (cobra command definitions),
not the CLI plugin entry point. `go build -o file pkg` against a non-main
package writes a Go archive; COPY and chmod accept it, but the kernel
cannot exec an ar-format file, so the Docker CLI plugin manager rejected
the binary and surfaced only as `docker: unknown command: docker compose`
in the v0.64.0 release smoke gate. The plugin's actual main package is at
./cmd (cmd/main.go calls plugin.Run from the cli-plugins framework), per
the upstream Makefile in v5.1.2.
Also fix the version ldflag to target the v5 module path: in v5 the module
moved from github.com/docker/compose/v2 to .../v5, so the previous
-X .../v2/internal.Version= silently no-op'd and `docker compose version`
would have reported an empty string even after the build target fix.
Add -trimpath and -s -w to match the upstream Makefile's release flags.
Add an inline ELF-magic sanity check inside the compose-builder stage so
this exact failure mode (non-main package emitting an archive) fails the
build at the producer instead of surfacing 200 lines later. Hex match via
`od -tx1` works on both busybox and GNU coreutils; the named-character
form (`od -c`) renders fields differently across the two.
Add a binary smoke step to the docker-validate PR job that mirrors the
release-time gate, so a regression in either the CLI or compose source
build is caught at PR review time rather than at tag-push time.
* fix(vex): match Trivy purl format for bundled docker/docker version
The compose plugin build target fix exposes a real consequence: now that
compose-builder produces a true ELF binary (rather than a Go archive),
Trivy can read the embedded Go module metadata and correctly reports the
bundled docker/docker dependency as `v28.5.2+incompatible`. The existing
VEX subcomponent ID omitted the `+incompatible` Go-module suffix, so
Trivy's purl matcher rejected the suppression and CVE-2026-34040 surfaced
in the docker-validate scan against the rebuilt image.
Update the @id to the literal version Trivy emits and bump the document
version per OpenVEX spec. The justification (compose acts as a CLI
client, never as a daemon, so the authz-hook path is unreachable) is
unchanged and continues to apply to v28.5.2+incompatible identically.
* docs(security): explain why CVE-2026-34040 cannot be upgraded out
A direct in-build upgrade attempt (`go get github.com/docker/docker@<sha
of docker-v29.3.1>`) fails with:
invalid version: go.mod has post-v1 module path
"github.com/moby/moby/v2" at revision f78c987a
moby/moby migrated its Go module path to `github.com/moby/moby/v2` on
the docker-29.x branch. The legacy `github.com/docker/docker` import path
is therefore frozen at v28.5.2+incompatible from Go's perspective; the
proxy.golang.org listing for that module path confirms no v29.x version
is resolvable. compose v5.1.2 (and v5.1.3, the latest tag at the time of
writing) both still import the legacy path, so the bundled docker/docker
library cannot be moved past v28.5.2+incompatible until upstream compose
migrates its imports.
Document the constraint in two places so a future maintainer (or future
me) does not re-attempt the same `go get` and arrive at the same dead
end:
- Dockerfile compose-builder stage: comment block above the build step.
- security/vex/sencho.openvex.json: expand the CVE-2026-34040 statement's
impact_statement to spell out the upstream module-split blocker. The
not_affected status (compose runs as a CLI client, never executes the
daemon authz hook code path) is the principled triage and remains the
correct OpenVEX call, not a deferred upgrade.
No build or runtime behaviour changes. Pure documentation enrichment.
* fix(security): mirror CVE-2026-34040 VEX statement in .trivyignore
Trivy reports `Some vulnerabilities have been ignored/suppressed` but
still fails the docker-validate scan on CVE-2026-34040, because the
OpenVEX statement's product identifier `pkg:oci/sencho` cannot match
the build-local image tag `sencho:pr-test`. Trivy does not generate an
OCI purl for images that lack a registry digest (aquasecurity/trivy
issue 9399), so VEX product matching is a no-op at PR and release scan
time. The dual-layer pattern documented in .trivyignore's header was
designed for exactly this case: VEX is the canonical, attested,
published triage record, and .trivyignore mirrors entries that scan-time
matching cannot resolve, with a comment pointing to the corresponding
VEX justification.
The CVE-2026-34040 OpenVEX statement is unchanged (status=not_affected,
justification=vulnerable_code_not_in_execute_path). The .trivyignore
entry is a build-time scan filter only, with a referencing comment so
auditors land on the VEX file as the source of truth. CVE-2026-39883
already follows this pattern and continues to.
* feat(security): add SBOM attestations, VEX document, and retire .trivyignore
Add OpenVEX triage document (security/vex/sencho.openvex.json) for the 5
residual CVEs vendored inside docker/compose v5.1.2 that were carried over
from the previous PR. All 5 are marked not_affected with justifications.
Configure Trivy in both CI and release workflows to consume the VEX document
via trivy.yaml so the same source of truth gates PR scans and release scans.
Delete .trivyignore, which is fully superseded by the VEX file.
Add two new release pipeline steps after image publication:
- CycloneDX 1.6 SBOM via anchore/sbom-action (also installs syft)
- SPDX 2.3 SBOM via syft directly (reuses OCI layer cache from prior step)
Both are attached as cosign OCI referrer attestations (keyless, OIDC-signed)
and uploaded as GitHub Release assets alongside the OpenVEX file.
Bump docker-publish.yml permissions from contents:read to contents:write,
required for softprops/action-gh-release to create Release assets.
Add docs/operations/verifying-images.mdx with copy-paste verification
commands for all supply-chain artifacts: signature, SLSA provenance,
CycloneDX SBOM, SPDX SBOM, OpenVEX, and Rekor entry. Update docs.json
navigation and expand the Supply chain security section in docs/security.mdx.
Add a Verifying Release Artifacts section to SECURITY.md.
* fix(vex): cover otel SDK CVE-2026-39883 in rebuilt Docker CLI binary
The rebuilt Docker CLI v29.4.0 vendors otel/sdk v1.42.0, which still
contains CVE-2026-39883 (BSD kenv PATH hijacking; fixed in v1.43.0).
docker-compose v5.1.2 vendors otel/sdk v1.38.0 separately. The original
VEX statement only covered the compose binary's location and version,
so Trivy's scan of /usr/local/bin/docker was not suppressed.
Add a second subcomponent entry for the CLI binary path with the
correct vendored version. The not_affected justification (BSD-only
code path; we ship linux/amd64 and linux/arm64 only) holds for both
binaries.
* fix(ci): use list form for vulnerability.vex in trivy.yaml
Trivy's config schema requires vulnerability.vex to be a list (mapped
to the multi-value --vex flag). The previous bare-string value was
silently dropped, so the OpenVEX document was never loaded and HIGH
findings already covered by VEX statements still failed the scan.
* fix(ci): mirror VEX CVE in .trivyignore for local-image scan
Trivy does not emit an OCI purl for locally-built images without a
RepoDigests entry (aquasecurity/trivy#9399), so OpenVEX product
matching against the CI build target sencho:pr-test resolves to no
artifact and every statement is silently dropped. The VEX document
remains the canonical triage record and is still attached as a cosign
attestation on the published image; this file just mirrors the single
CVE that surfaces on the local scan so CI does not block on a finding
already triaged in VEX. Updates the Trivy step comment to document
the relationship between the two files.
* feat(security): rebuild Docker CLI/Compose from source, pin base image digests
Build Docker CLI v29.4.0 and Compose v5.1.2 from source against Go 1.26.2
to resolve 7 CVEs that were accepted in .trivyignore:
- CVE-2026-32280/32281/32282/32283/33810: Go stdlib x509/TLS and DNS
issues in the upstream static CLI binary (compiled with Go 1.26.1).
All fixed by rebuilding with Go 1.26.2.
- CVE-2026-33186: grpc 1.78.0 in Docker CLI. Eliminated from the SBOM
by building with the patched Go toolchain and updated module graph.
- CVE-2026-33671: picomatch ReDoS in npm bundled with node:22-alpine.
Resolved by removing npm/npx from the runtime image entirely (npm is
only needed at build time).
Remaining 5 entries in .trivyignore are vendored deps inside Compose
v5.1.2 (buildkit, moby/docker, otel) that cannot be patched without an
upstream Compose release. These will be expressed as OpenVEX
not_affected statements in the follow-up PR (feat/security-sbom-vex).
Also in this commit:
- Pin all Dockerfile FROM lines to @sha256: digests (node:22-alpine,
tonistiigi/xx, golang:1.26-alpine) to prevent silent base image
changes between scan and publish.
- SHA-pin all GitHub Actions in docker-publish.yml and ci.yml that
were previously referenced by mutable @vN tags.
- Add a binary version smoke test to confirm docker/compose produce
expected output before the multi-arch push proceeds.
* fix(docker): fix cli-builder vendor mode and compose-builder cache path
docker/cli v29.4.0 uses CalVer and ships vendor.mod instead of go.mod,
so plain `go build` fails with "cannot find main module." Fix: copy
vendor.mod -> go.mod and vendor.sum -> go.sum before building, then
pass -mod=vendor so all deps come from the vendored tree with no network
access. Cache mount is not needed with vendor mode and is removed.
compose-builder used /root/go/pkg/mod as the cache mount target, but
golang:alpine sets GOPATH=/go, so the module cache lives at /go/pkg/mod.
The wrong path caused a silent cache miss on every build. Corrected.
Replace the long-lived DOCS_REPO_TOKEN personal access token with
short-lived installation tokens minted per-job from the sencho-token-app
GitHub App (owned by the AnsoCode org, installed on both Sencho and
sencho-docs).
Why this is better than the PAT:
- Scoped per-job to only the repo the job actually touches, not one
token that works everywhere. update-screenshots and release-please
mint against Sencho; sync-docs mints against sencho-docs.
- Scoped per-job to only the permission scopes the job actually needs
(contents:write and sometimes pull-requests:write), not the broader
scope a PAT carries.
- Auto-revoked at job end by the action's post-run step, so a leaked
log line or artifact is useful for at most the remainder of that job.
- Not tied to any individual user account: no more risk of the token
silently dying when the human who minted it leaves or rotates keys.
- Like a PAT but unlike GITHUB_TOKEN, installation tokens DO trigger
downstream workflow runs, so the release-please tag push still
cascades into docker-publish.yml exactly as before.
Jobs migrated:
- release-please.yml: single mint, passed to release-please-action.
- ci.yml update-screenshots: one mint reused by checkout, peter-evans
create-pull-request, and the gh pr merge step.
- ci.yml sync-docs: one mint targeting sencho-docs, fed to the git
clone URL as x-access-token.
DOCS_REPO_TOKEN can be deleted from repo secrets after the first full
release cycle under this migration validates release-please,
update-screenshots, and sync-docs all succeed end-to-end.
actions/create-github-app-token pinned to v3.0.0 at SHA
f8d387b68d61c58ab83c6c016672934102569859, consistent with the
SHA-pinning convention established in the supply-chain hardening PR.
The runtime stage's `apk upgrade --no-cache` layer was being reused from
the buildx gha/registry cache indefinitely, so newly-patched Alpine
packages (e.g. openssl CVE fixes) could sit behind a stale layer until
an unrelated Dockerfile change invalidated it by coincidence. This
surfaced as Trivy hard-failing on CVE-2026-28390 (libcrypto3/libssl3
DoS in CMS) on the github-app-token-migration PR, even though upstream
Alpine had already published 3.5.6-r0 with the fix.
Add an APK_CACHE_BUST build-arg that CI sets to the current UTC date
(YYYY-MM-DD). Each calendar day the arg value changes, which changes
the RUN layer's hash, which forces buildx to re-execute `apk upgrade`
and pick up whatever Alpine has published that day. All three build
invocations (PR docker-validate, release pre-publish scan, release
multi-arch push) pass the same value, so the scan and the published
artifact always share a layer and Trivy never scans stale bits.
Default value `unset` lets local developers `docker build` without
the arg; production CI always supplies the date.
The backend CI job already runs `tsc` against backend/src/ and only
marks the job green after build + tests + lint + audit all pass. The
E2E job then checked out the same source and ran the same tsc again
inside the start-app composite.
Upload backend/dist/ as a 1-day retention artifact at the end of the
backend job, download it in the E2E job before invoking the composite,
and gate the composite's "Build backend" step on a new
`skip-backend-build` input. The update-screenshots job (which also
uses the composite but has no upstream backend job) leaves the input
at its default of 'false' and continues to build from source.
Savings are modest (~10-15s per E2E run, on top of what setup-node's
~/.npm cache already buys us) but the real win is the contract:
the dist/ that E2E exercises is now byte-for-byte the same artifact
that the backend job verified, eliminating a whole class of "works
in backend job, breaks in E2E" drift.
Skip the backend, frontend, docker-validate, and e2e jobs on the
chore/refresh-screenshots auto-PR opened by the update-screenshots job.
Screenshot PRs only touch docs/images/ and cannot affect build, lint,
or test surfaces, so running the full PR CI suite on them burns ~6
action minutes per release for zero signal. This mirrors the existing
release-please--branches--main--components--sencho skip.
Also short-circuit sync-docs: when a push to main does not touch any
file under docs/, skip the clone, rsync, and commit steps. rsync
--delete would be a no-op anyway, but spinning up the runner and
cloning sencho-docs still burns ~30s per non-docs merge, which adds
up across multiple merges per day. Requires fetch-depth: 2 so the
detect step can diff HEAD~1 HEAD.
Finally, correct the scan-build comment in docker-publish.yml: the
push-build's amd64 leg reuses layers from the buildkit daemon's
in-memory cache (same job, same daemon), not from the shared registry
buildcache. The cache-from pull is a cold-start fallback for the
first-ever run.
* ci: hard-fail PR and release scans on unacknowledged HIGH/CRITICAL CVEs
Make Trivy a real gate instead of an advisory signal:
- PR CI (docker-validate) no longer uses `continue-on-error: true` on the
Trivy step, so any HIGH/CRITICAL finding not in `.trivyignore` fails the PR.
- The release pipeline (docker-publish.yml) now builds an amd64-only scan
image into the local daemon before the multi-arch push-build, re-runs
Trivy against that exact artifact, and only proceeds to the push if the
scan passes. This closes the gap where a CVE landed between PR merge and
release-time rebuild.
- New `.trivyignore` at repo root is the single source of truth for
acknowledged CVEs across both workflows; it starts empty so the first CI
run surfaces the full list, which we then populate with justifications.
* ci: populate .trivyignore with initial HIGH/CRITICAL acknowledgements
First CI run on the hard-fail Trivy policy surfaced 7 unacknowledged
findings. Each has been reviewed and justified inline:
- 6 CVEs in the statically-linked Go modules inside docker-compose v5.1.1
(github.com/docker/docker, buildkit, otel/sdk x2, grpc). These are
transitively bundled and cannot be bumped without an upstream Compose
rebuild. The grpc CVE is already explicitly acknowledged in the
Dockerfile rationale block at Dockerfile:108-111.
- 1 CVE in picomatch 4.0.3 bundled inside the npm CLI that ships with
node:22-alpine. npm is only invoked at build time against our own
package.json, so the ReDoS vector is not reachable.
Every entry has a revisit trigger (next Compose release or next Alpine
node bump).
When an E2E test flakes in CI, the backend and frontend stdout is gone
the moment the step exits. That makes a failed run extremely hard to
triage: you get the Playwright report but nothing from the services
underneath it. Redirect both dev servers to files under the workspace's
ci-logs/ directory and include that directory in the failure-case
artifact upload, so the report and the service logs travel together.
Log files live under $GITHUB_WORKSPACE rather than /tmp because
actions/upload-artifact@v4+ computes a common parent directory across
all path entries. Mixing repo-relative paths with /tmp paths makes '/'
the common root, which v4+ then strips from the artifact, silently
dropping the absolute entries. Keeping everything under the workspace
avoids that footgun.
`stdbuf -oL -eL` forces line-buffered output on both services. Node
stdout is block-buffered when redirected to a regular file, so a crash
or abort can lose the final few hundred lines - exactly the output you
need when debugging a CI failure.
Also remove the duplicated `E2E_USERNAME: admin` / `E2E_PASSWORD:
password123` env blocks from the e2e and update-screenshots jobs.
Those values were already the fallback defaults in e2e/helpers.ts
(`process.env.E2E_USERNAME ?? 'admin'`), so the env blocks were doing
nothing on the default path and created two sources of truth for the
same literals. e2e/helpers.ts remains the canonical source; callers
that want to override the creds still set E2E_USERNAME / E2E_PASSWORD
as job-level env.
Pin three third-party actions to full commit SHAs with version comments
so dependabot can update them in place without exposing the pipeline to
upstream tag tampering:
- aquasecurity/trivy-action@v0.35.0
- peter-evans/create-pull-request@v8.1.1
- googleapis/release-please-action@v4.4.0
Add a workflow-level permissions: contents: read default to ci.yml, with
update-screenshots keeping its explicit contents: write + pull-requests:
write override. Add the same default to docker-publish.yml (single job).
Expand .dockerignore to explicitly exclude .env*, *.key, *.pem, .github/,
docs/, website/, e2e/, tests/, test-results/, and playwright-report/ as
defense in depth against accidental build-context leaks. None of these
paths are read by any COPY step in Dockerfile.
* ci: optimize workflows to cut redundant runs and add timeout protection
Skip validation jobs (build, test, lint, Docker, E2E) on push-to-main
since code was already validated on the PR. Skip CI entirely for
release-please PRs (version bump only). Add timeout-minutes to all jobs
to prevent hung runners from burning 360 min (GitHub default). Add npm
caching to the start-app composite action. Add concurrency control to
docker-publish. Group Dependabot updates to batch PRs per ecosystem.
Estimated savings: ~10 min per release cycle, ~30-40 min/day.
* chore: add tsbuildinfo to gitignore
gh pr merge --auto requires branch protection with required status
checks. This repo has no branch protection, so GitHub rejects --auto
with "clean status" error. Remove --auto to merge immediately.
Adds the ability to schedule recurring Docker operations (stack restarts,
fleet snapshots, system prunes) via cron expressions with full execution
history logging. Includes Run Now for on-demand execution.
* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro)
Introduces three Pro-tier features:
- RBAC: Multi-user system with admin/viewer roles, user management UI,
automatic migration from single-admin credentials, viewer restrictions
across the entire UI (read-only editor, hidden action buttons)
- Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic
rollback on health probe failure, manual rollback button, health probes
added to stack updates, webhook-triggered deploys use atomic rollback
- Fleet-Wide Backups: Point-in-time snapshots of compose files across all
nodes (local + remote), stored centrally in SQLite, per-stack restore
with optional redeploy, graceful handling of offline nodes
* fix(settings): use correct ProGate prop name in UsersSection
* fix(settings): remove unused isPro prop from UsersSection
* fix(auth): fetch user info after login and setup so isAdmin is set correctly
* feat(pricing): revise pricing strategy and enforce variant-based seat limits
Raise Personal Pro from $49/yr to $69/yr with 3 viewer seats (up from 1).
Add $15/mo billing option for Team Pro. Mark lifetime pricing as a
90-day early-adopter offer. Store Lemon Squeezy variant_name on
activation/validation and enforce seat limits server-side per variant.
* feat(licensing): add Lemon Squeezy checkout, webhook, and billing portal integration
Server-side checkout URL generation (POST /api/checkout) with admin email
pre-fill and instance_id custom data. HMAC-SHA256 verified webhook endpoint
(POST /api/webhooks/lemonsqueezy) handling order, subscription, and payment
lifecycle events for automatic license activation. Customer billing portal
link stored from webhook events and exposed via GET /api/billing/portal.
In-app checkout buttons in Settings with manual license key fallback.
* fix(licensing): exempt Lemon Squeezy webhook from auth middleware
The catch-all auth middleware on /api/* was blocking the public webhook
endpoint. Added /webhooks/lemonsqueezy to the exemption list alongside
/auth/* and /webhooks/:id/trigger.
* feat(pricing): update pricing to final live rates
Personal Pro: $7.99/month, $69.99/year, $249 lifetime.
Team Pro: $49.99/month, $499.99/year, $1,499 lifetime.
Added personal_monthly checkout variant across backend, frontend, and website.
* refactor(licensing): remove server-side checkout/webhook for self-hosted model
Sencho is self-hosted — each user runs their own instance, so there is
no central server to receive webhooks or hold the store API key. Replaced
in-app checkout buttons with a "View Pricing" redirect to sencho.io and
kept manual license key activation as the primary flow.
- Delete LemonSqueezyService (checkout, webhook, HMAC verification)
- Remove POST /api/checkout, GET /api/billing/portal, POST /api/webhooks/lemonsqueezy
- Remove raw body parser and auth exemption for webhook route
- Remove all LEMONSQUEEZY_* env vars from .env.example
- Replace checkout buttons in SettingsModal with single "View Pricing" button
- Simplify LicenseContext checkout to open sencho.io pricing page
- Update licensing docs to reflect website-based purchase flow
* chore: normalize em-dashes to hyphens across codebase (linter)
* chore: remove accidentally tracked directories from index
* chore: add comprehensive .gitignore
* ci: update CI workflow for GitHub Flow
- Change triggers from develop to main (PRs to main + pushes to main)
- Add concurrency controls to cancel stale runs
- Update docker/build-push-action to v6
- Add descriptive job names for branch protection status checks
- Update screenshot refresh and docs sync to trigger on main pushes
* ci: update docker-publish for GitHub Flow
- Remove develop branch trigger (no more dev tag)
- Keep v* tag trigger for releases
- Update docker/build-push-action to v6
* docs: add community and governance files
- CONTRIBUTING.md with dev setup and PR guidelines
- SECURITY.md with vulnerability reporting policy
- CODE_OF_CONDUCT.md (Contributor Covenant v2.1 reference)
- PR template with conventional commits checklist
- Issue templates for bug reports and feature requests
- CODEOWNERS defaulting to @AnsoCode
- Dependabot config for npm (root, backend, frontend) and GitHub Actions
* docs: add README with badges, quick start, and contributing section
* chore: add LICENSE placeholder and open license decision issue (#100)
* docs: update CLAUDE.md for GitHub Flow branching model
- Replace develop-based Git Flow with GitHub Flow (main only)
- All branches now created off main, PRs target main
- Simplify release checklist (no develop-to-main merge step)
- Update testing strategy to reference Vitest and Playwright
- Fix docs.json reference (was mint.json)
* chore: track CLAUDE.md in version control
Remove CLAUDE.md from .gitignore so project workflow instructions
are versioned alongside the code they govern.
* docs: add MANUAL_STEPS.md for GitHub settings that require UI configuration
- Add monaco-editor to frontend deps and configure @monaco-editor/react
to use the local bundle via loader.config({ monaco }) instead of
fetching from cdn.jsdelivr.net (blocked by CSP scriptSrc: self)
- Wire editorWorker via Vite ?worker syntax — blob: URLs already
permitted by existing workerSrc CSP directive, no CSP changes needed
- fix(ci): use DOCS_REPO_TOKEN in release-please so tag creation
cascades to docker-publish.yml (GITHUB_TOKEN cannot trigger workflows)
- fix(ci): auto-merge screenshots PR via gh pr merge --auto --squash
rsync --delete removes everything in the destination that is absent from
the source. sencho/docs/ has no .git directory, so rsync was deleting
sencho-docs/.git/ on every run. The commit step then had no git repo to
work with, giving 'fatal: not in a git directory'.
Fix: add --exclude='.git' to the rsync invocation.
update-screenshots:
- Add pull-requests: write to job permissions (required by
peter-evans/create-pull-request to open a PR)
- Switch checkout and create-pull-request from GITHUB_TOKEN to
DOCS_REPO_TOKEN (classic PAT with repo scope); GITHUB_TOKEN is
blocked from creating PRs against protected branches
sync-docs:
- Replace actions/checkout + init fallback with a single bash step that
runs git clone and falls back to git init on failure; actions/checkout
on an empty repo creates a .git in a broken state (no HEAD/branch),
which the previous [ ! -d .git ] guard never caught, leaving the
commit step with no valid working tree
- Drop --global safe.directory config (no longer needed once we own
the git setup ourselves)
update-screenshots: replace direct git push to develop (blocked by branch
protection) with peter-evans/create-pull-request@v6, which opens or
updates a chore/refresh-screenshots PR instead.
sync-docs: add continue-on-error on the sencho-docs checkout so an empty
repo doesn't abort the job. A new init step runs git init -b main and
re-adds the remote when .git is missing. Final push uses
git push origin HEAD:main to work on both first-run (empty repo) and
subsequent runs (existing branch).
- Add sync-docs CI job: runs on push to main, copies /docs into sencho-docs repo via DOCS_REPO_TOKEN
- Scaffold /docs with mint.json, getting-started/introduction.mdx, getting-started/quickstart.mdx, features/overview.mdx
- Update CHANGELOG
- Split serial build-and-test job into parallel backend, frontend, docker-validate, and e2e jobs
- backend job: build → test → lint (new ESLint) → npm audit --audit-level=high
- frontend job: build → lint → npm audit --audit-level=high
- docker-validate job: builds image on every PR without pushing; Trivy scans for CRITICAL/HIGH CVEs (informational)
- e2e job: runs full Playwright suite against live dev servers after backend+frontend pass
- Add backend/eslint.config.mjs and lint script to backend/package.json