Skip to content

Releases

v0.16.3

Choose a tag to compare

@ahliweb ahliweb released this 10 Oct 08:22
73ca70f

AW Business Platform Wave A documents and owner decisions

The owner answered decisions O1 to O12 for epic #280 on 10 October 2026. They also chose an upstream Booking v1 scope change for nightly stays (W0, ahliweb/awcms#931). The awcms-one Wave A documents were written from these decisions:

  • docs/aw-business-platform-prd.md: platform blueprint and PRD for the awcms-one-owned parts (A6, #336).
  • docs/aw-business-platform-metrics.md: metric contracts for revenue, occupancy, utilization, retention and productivity (A7, #337).
  • docs/aw-business-platform-threat-model.md: adapter threat model, privacy analysis and regulatory applicability notes. It makes no compliance claim (A8, #338).
  • docs/aw-business-platform-dor.md: records the answers and the A9 cross-spec review (#339).
    • Verdict: the ADR-0040 D7 full gate is not passed. DoR artifacts 4, 5, 7 and 8 are tracked in #356 to #359, with #353 to #355 as further prerequisites.
    • Under the D7 carve-out, the commerce-only Wave B issues #360 to #364 are open.
    • #339 stays open as the gate tracker.
  • The awcms-one-commerce skill now says that commerce domain-event consumers are declared in the module descriptor (ADR-0134 in apps/cms/docs/adr/), not in upstream's consumer-registry.ts.

This change is documentation only: it adds no code, module, migration or OpenAPI path (ADR-0040 D7).

Sync apps/cms to AWCMS v10.7.0 and adopt descriptor-declared domain-event consumers

Subtree sync of apps/cms from upstream 66c74273 (AWCMS v10.7.0, 7 commits; issue #347). No new migration.

  • Upstream ADR-0134 (ahliweb/awcms#926): consumers are declared in ModuleDescriptor.domainEventConsumers (module contract 4.2.0) and the runtime builds its registry from the composed module list; the new domain-events:consumers:check gate is in the apps/cms check chain.
  • This repo's standing divergence in domain-event-runtime/infrastructure/consumer-registry.ts is retired: upstream's file is taken unchanged and the four commerce consumers (commerce.order_paid_entitlement_grantor, commerce.order_paid_loyalty_earner, commerce.order_cancelled_loyalty_reverser, commerce.inventory_stock_cache_projector) are declared in the commerce module descriptor with unchanged names. The domain_event_runtime -> commerce module-boundary exception is gone.
  • Wave A design packs for booking, hr_payroll and delivery plus provisional cross-domain AsyncAPI events arrive as documentation.
  • AGENTS.md / AGENTS.id.md "Last sync" row and divergence list, and docs/aw-business-platform-dor.md DoR item 9, are updated.

v0.16.2

Choose a tag to compare

@ahliweb ahliweb released this 08 Oct 11:20
99da37b

ADR-0040 accepted by the owner

Owner decision O0 on issue #335 accepted ADR-0040 (AW Business Platform capability ownership and boundaries) as written: D1, D2 and D4 stand unamended, so Booking, the hr_payroll family and generic delivery stay upstream-first in ahliweb/awcms (#915 to #918).

  • ADR-0040 and its Indonesian mirror are set to Accepted with a decision record; the ADR index row follows.
  • docs/aw-business-platform-dor.md drops the answered O0 row and marks Wave A item A1 done. O1 to O12 remain open, and A6 to A8 stay blocked on them.
  • Documentation only: no code, module, migration or OpenAPI path is added (ADR-0040 D7).

Sync apps/cms to upstream AWCMS v10.6.0

apps/cms now embeds ahliweb/awcms main at 61507bb7 (AWCMS v10.6.0), two commits past the previous sync (a0dabcd9).

  • idn-admin-regions dataset rollback (ahliweb/awcms#914, #922): now() is the transaction start, so the active dataset could be stamped equal to or earlier than the one it superseded, and the strict activated_at < filter then found nothing to roll back to. Rollback now takes the latest superseded dataset by (activated_at, id) with no comparison. Covered by a new integration test.
  • Version bump to 10.6.0 with upstream's CHANGELOG.md and PROJECT_STATE updates. No new migrations, no dependency changes.
  • No standing local divergence was resolved or added. The only conflict was in apps/cms/docs/PROJECT_STATE.md and its Indonesian mirror (upstream's version and release-note rows against this embed's module and migration counts); both lineages were kept and the mirror restamped.

v0.16.1

Choose a tag to compare

@ahliweb ahliweb released this 08 Oct 02:52
2aaa499

AW Business Platform: capability ownership ADR and DoR tracker (epic #280, Wave A)

Documentation only; no code, migration or module is added. ADR-0040 (Proposed) re-validates the epic's reuse/extend/new matrix against the current tree and places each capability: upstream ahliweb/awcms first (Booking, hr_payroll, generic delivery), awcms-one adapters after the upstream sync, consumer repositories for vertical semantics. It also records that ADR-0029 already settles the invoice boundary.

  • New: docs/adr/0040-aw-business-platform-capability-ownership-and-boundaries.md and its Indonesian mirror; ADR index rows added.
  • New: docs/aw-business-platform-dor.md (and mirror), the ten DoR artifacts with owner repository, status and owner decisions, plus the proposed Wave A child issues.
  • docs/status.md "Not here yet" and docs/README.md point to the tracker.

Commerce orphan detection reads balances through the inventory port

listLedgerOrphans (the orphans section of the stock reconciliation) no longer SELECTs awcms_inventory_balances directly. It pages InventoryLedgerPort.listBalances (synced from upstream awcms#913) and classifies each page against commerce's own tables in one query, so commerce no longer reads any inventory table. The report is unchanged: same reasons, same ordering, same ORPHAN_LIMIT cap.

  • The walk is bounded at ORPHAN_SCAN_MAX_PAGES (100 pages of 500 balances); stopping at that cap sets truncated: true, which the old unbounded query never needed.
  • The sql/947 worker SELECT grant on awcms_inventory_balances is now unused by commerce and is deliberately left in place.
  • ADR-0038's "Orphan detection" paragraph and the commerce README are updated.

apps/cms synced to ahliweb/awcms a0dabcd

apps/cms now carries upstream main at a0dabcd9: the sharp, shell-quote and source-map-js advisory fixes (ahliweb/awcms#920) and InventoryLedgerPort.listBalances, a paged, read-only listing of ledger balances (ahliweb/awcms#913). No new migration, no conflict beyond the regenerated apps/cms/docs/awcms/repo-inventory.md.

v0.16.0

Choose a tag to compare

@ahliweb ahliweb released this 07 Oct 09:17
ed8a6e4

Commerce bundles: item kits stocked through their components (issue #290, ADR-0036)

A shop can now sell a set - "two coffees and a syrup at one price" - without a stock counter on the set that somebody keeps in step by hand. A bundle is a product with kind = 'bundle': no variants, no stock of its own, not a service product, not flash-sale eligible, and made of 1-20 component lines (a product, a variant when that product has live variants, and a quantity per bundle). Nesting is refused by trigger, so a cycle cannot exist. Decision and rejected alternatives: docs/adr/0036-bundles-are-component-stocked-products-sold-as-one-line.md.

  • New: sql/953 to sql/955 under apps/cms - kind / bundle_pricing / bundle_discount_percent on awcms_commerce_products, awcms_commerce_bundle_components (composite tenant FKs, FORCE RLS, the nesting/variant/flash-sale triggers), the append-only awcms_commerce_order_item_components snapshot, and the awcms_worker grant the expiry restock needs. Existing tenants need no backfill; every product is standard.
  • Pricing is fixed (the product's own price) or derived (the components' list prices x quantity, less a percent, half-up to the cent). Availability is min floor(component stock / quantity) and is reported as the bundle's stock, so the storefront, POS search and barcode scan need no new field.
  • A bundle sells as one order line with an immutable component snapshot (units, text as sold, the line value split across components to the cent). The components - never the bundle - move stock: a counter decrement after locking and re-quoting in counter mode, a sale per component with source line <orderItemId>:c<position> through the inventory adapter in ledger mode. Cancel, expiry and a whole-bundle return put exactly those components back from the snapshot; editing the bundle afterwards changes nothing about an old order. Tax is one line by the bundle's own category.
  • API: POST/PATCH /api/v1/commerce/products accept kind, bundlePricing, bundleDiscountPercent and bundleComponents (ids or SKUs); product reads gain kind, bundlePricing, bundleDiscountPercent and bundle; a cart quote line for a bundle carries bundle.components. No new permission (commerce.products.*).
  • Admin: a Bundle toggle, pricing strategy, discount and a SKU x quantity contents textarea on the product form. Storefront: an "Isi paket" list on the product page.
  • Not here (recorded in the ADR): per-component tax classes, partial component return, a bundle report, nested bundles, a cart-level combined component check in counter mode.
  • Backward compatible. Nothing changes for an existing product or order.

Commerce stock on the upstream inventory ledger (issue #282, ADR-0038)

A tenant's commerce stock was one integer on each product and variant. Upstream AWCMS now ships a multi-location stock ledger (module inventory, embedded by the v10.5.0 sync), and a store can move onto it without a second source of truth: in the new ledger mode the ledger is authoritative and stock becomes a write-through cache of it, so the storefront, cart quote, POS, barcodes and admin lists read exactly what they read before. The default, counter, is today's behaviour byte for byte. Decision and rejected alternatives: docs/adr/0038-commerce-stock-is-a-write-through-cache-of-the-inventory-ledger.md; operator runbook in docs/deployment.md.

  • New: apps/cms/sql/947_awcms_commerce_inventory_adapter.sql — inventory_mode / inventory_location_id (composite FK to the inventory location) / inventory_mode_changed_at on awcms_commerce_store_settings, two commerce.inventory.{read,configure} permissions, and the awcms_worker inventory grants the expiry job and the cache projector need (a column-level UPDATE (updated_at) on locations, never a table-level one). Existing tenants run bun run identity-access:permissions:backfill to receive the permissions.
  • New: bun run commerce:inventory:cutover --tenant <id> --location <id> [--commit] — dry-run by default; one transaction posts an opening per stock unit, flips the mode and verifies, behind an exclusive per-tenant mode lock. A second run is refused.
  • In ledger mode an order or POS sale posts sale, a cancel/expiry restock and a return's restock post sale_return, through the ledger port inside the caller's transaction, sorted against deadlocks and in a savepoint: an out-of-stock cart is still 409 CART_CHANGED / PosCartChangedError with nothing written, and eight parallel orders for the last unit sell exactly one. Any other ledger refusal is 409 INVENTORY_UNAVAILABLE.
  • A movement made by something else (a receipt, an adjustment, a transfer) reaches the cache through the new commerce.inventory_stock_cache_projector consumer, which re-reads the ledger and never trusts the payload. This is a recorded local divergence in consumer-registry.ts (see AGENTS.md).
  • A product/variant edit, a create with stock, or a CSV stock change that would change a count is 409 STOCK_MANAGED_BY_INVENTORY in ledger mode (the unchanged value is accepted).
  • New endpoints: GET /api/v1/commerce/inventory, GET …/inventory/reconciliation, POST …/inventory/resync, POST …/inventory/rollback (commerce.inventory.read / .configure). commerce now declares a dependency on inventory.
  • Low stock is upstream's inventory.low_stock projection, not a commerce report (supersedes the ADR-0035 D1 note for #282).
  • Backward compatible, opt-in per tenant. Nothing changes until an operator runs the cut-over; POST …/inventory/rollback returns a tenant to the counter.

Procurement feeds commerce stock (issue #283, ADR-0038 addendum)

Upstream awcms's procurement module (suppliers, receiving, supplier returns, requisitions, transfers; upstream ADR-0128) already posts to the inventory ledger. This change makes it work with commerce stock on a tenant in ledger mode, with no new table or migration:

  • Convention. A procurement line stocking commerce goods uses itemType commerce.variant (variant uuid) or commerce.product (uuid of a product with no live variant), unit unit, at the sales location (or another location and then a transfer). New GET /api/v1/commerce/inventory/items?q=<sku or name> (commerce.inventory.read, keyset-paged, at most 50) resolves a SKU or name to { itemType, itemRef, sku, name, variantName, unitCode }.
  • Orphan detection. GET /api/v1/commerce/inventory/reconciliation now also returns, on its first page, orphans: non-zero commerce.* ledger balances at the sales location naming no live unit (not_found, product_has_variants, wrong_unit, unknown_item_type), capped at 100 with truncated.
  • Counter mode. A procurement receipt on a counter tenant stays in the ledger and does not change commerce stock - cut over to ledger first (runbook in docs/deployment.md; existing tenants run identity-access:permissions:backfill for procurement.*).
  • Reporting. Upstream's procurement.receiving / procurement.suppliers projections are the receiving reports (supersedes the ADR-0035 D1 note for #283).
  • Backward compatible. The orphans field is additive; nothing changes for a tenant that never uses procurement.

Commerce tax moves onto the tax module, per tenant (issue #293, ADR-0039)

Until now a store's tax was one number, payment.tax.percent, applied once to subtotal - voucher discount. Upstream AWCMS's generic tax module (awcms ADR-0127, arrived with the v10.5.0 sync) can express what that number cannot — a rate that changes on a date, exempt and zero-rated categories, prices that already include tax, and a refund that takes back exactly the tax that was charged. This change is the adapter, behind a per-tenant mode so nobody moves until an operator moves them: flat (the default) is the existing arithmetic byte for byte, engine asks the module.

  • New: awcms_commerce_store_settings.tax_mode / tax_profile_code, awcms_commerce_products.tax_category_code and awcms_commerce_orders.tax_snapshot_id (a composite FK to the tax snapshot ledger), all in apps/cms/sql/948_awcms_commerce_tax_adapter.sql — nullable or defaulted, nothing backfilled. commerce now declares tax in its module dependencies.
  • In engine mode the cart, POS and quotation quote use the module's calculator (the store's Asia/Jakarta business date picks the effective-dated version); a voucher is allocated across the lines in cents so, with the same percentage, the engine equals the flat figure exactly — proven by a seeded property test. A storefront or POS order finalises exactly one tax snapshot in its own transaction and stores its id; a return reverses the returned units' tax, and a cancelled or expired order the rest, from the ORIGINAL snapshot, never from today's rule. A tax the engine cannot answer blocks checkout instead of selling untaxed.
  • New ops-only bun run commerce:tax:cutover (dry-run by default; --commit, --tenant, --sample, --since, --rollback): derives the store-default version from the current settings, re-prices the tenant's recent orders in shadow and refuses to flip unless every one matches to the cent. The flip and its rollback are audited (tax_mode.update). Runbook: docs/deployment.md, including bun run identity-access:permissions:backfill so existing tenants' owners hold the tax.* permissions.
  • Additive API shapes: the cart quote's tax gains mode, inclusive, error and engine; admin store settings GET gains the read-only taxMode / taxProfileCode; product create/update/admin read gain taxCategoryCode. The product form has a Tax category field; store settings shows the mode.
  • awcms_worker gains INSERT, UPDATE on awcms_tax_snapshots so the expiry job can reverse an expired engine order's tax (the append-only trigger still refuses real updates); a setting...
Read more

v0.15.0

Choose a tag to compare

@ahliweb ahliweb released this 04 Oct 04:54
f10f05f

Close the devalue/fast-uri advisories; accept the unfixable http-cache-semantics one with a date

New advisories against transitive dependencies turned main red on
local-ci/check-toko (bun audit) and local-ci/check-cms
(deps:audit:check) without any change in this repository — every open PR
inherited the failure.

  • devalue 5.9.2 → 5.9.3 and fast-uri 3.1.7 → 3.1.8, both inside every
    dependent's declared range, so bun audit fix closed them in bun.lock
    alone.
  • http-cache-semantics (GHSA-ch52-4w7c-c8xp) has no fixed release at all.
    Its only consumer is astro's build-time remote-image cache, never a shared
    request-time cache, so it is accepted as a dated, reasoned exception (review
    by 2026-11-03) in apps/cms's deps:audit:check list and in a new root
    tools/ci/dependency-audit-exceptions.json, which local-ci/check-toko and
    local-ci/security (the two legs that run bun audit) now pass to
    bun audit --ignore, and which fails either leg once an entry's review date
    has passed.

Typed catalog attributes, safe attribute filtering, validated CSV import/export

Issue #291 (epic #281, OSPOS parity). The catalog gains tenant-defined, typed custom attributes, attribute filtering that cannot be turned into SQL injection, and a CSV import/export that validates before it writes. Everything is inside the existing commerce module (ADR-0008) and takes migrations 960–964 of the reserved range (ADR-0015). The decisions and the measured query plans are in ADR-0027.

  • Attributes are typed columns, not a JSON blob. Definitions (key, labels, a closed per-type constraint schema, searchable/filterable/visibility flags, product/variant applicability) and values (text, whole number, decimal, boolean, date, choice list) with a strict ASCII grammar: 1234.5 is a number, 1,5 is a 400 — a persisted number never depends on a locale.
  • Filtering is allowlisted by construction. attr=<key>:<op>:<value> resolves the key to a definition id in the database, parses the operand with the same typed grammar, and maps each (type, operator) pair to one literal SQL template with bound parameters. There is no code path from a request to a SQL identifier or expression, and a test throws injection payloads at keys, operators and values.
  • The storefront API only ever sees public attributes. Additive attributes[] on products and variants, filterable && visible_public filtering, and q search over searchable attributes of the same audience; existing fields are untouched.
  • Import is dry-run → report → apply, all-or-nothing and replay-safe. Same planner for both, one savepoint, Idempotency-Key, expectedSha256 to bind apply to the reviewed file, matched on sku, no media column, no network. Export neutralises spreadsheet formulas.
  • A new import access action (high-risk, beside export), permissions commerce.attributes.read|manage and commerce.products.import|export.
  • Operators: run bun run db:migrate (adds five migrations, no data change). Grant the new permissions to the roles that manage the catalog; existing tenants do not gain them automatically.
  • Review hardening. The admin q search and the CSV export only reach non-public attributes for a caller who holds commerce.attributes.read; the import accepts exactly text/csv (no CORS-safelisted text/plain); an apply claims its Idempotency-Key before writing, so concurrent applies replay or 409 instead of failing with a 500.

Held sales, quotations, work orders, and numbered receipts/invoices (issue #286, ADR-0029)

The sale had no documents around it: no way to park a cart, quote a price, track a service job, or print a numbered receipt or invoice, and the OSPOS way of getting those (one overloaded sale row) was ruled out. They now exist as separate records around the unchanged order, with seven tables (awcms_commerce_document_sequences, …_held_sales, …_quotations, …_quotation_versions, …_work_orders, …_work_order_events, …_documents — migrations 980–982 under apps/cms/sql/, starting at apps/cms/sql/980_awcms_commerce_documents_schema.sql). The "why", the invoice-versus-accounts-receivable boundary and the options weighed are in ADR-0029.

  • New: park a POS cart and resume it (lines only — no price, no stock reserved, owned by the cashier, expires after 24 hours by default, single-use resume); create a priced, versioned quotation with a validity period, accept it (which pins the version shown) and convert it into an order through the ordinary POS path (created at today's price with the balance due; a changed total is refused unless accepted; exactly one order per quotation, race-safe and idempotent); run work orders through a status machine with an append-only history, optionally from an accepted quotation; issue a numbered, immutable receipt or invoice for a finalized order (INV-2026-000042 — gapless per tenant, type and UTC year, allocated in the same transaction as the row) and render it as JSON, a 42-column plain-text receipt, or a print-ready, script-free HTML page under a locked-down CSP. All mutations take an Idempotency-Key; events awcms.commerce.quotation.{accepted,converted}, awcms.commerce.work_order.status_changed, awcms.commerce.document.issued; admin screen /admin/commerce-documents and hold/resume controls on /admin/commerce-pos.
  • No second monetary authority: a document's money is a copy a database trigger verifies against the order, and a quotation never writes an order itself. A commercial document is not an accounts-receivable invoice — that stays a future, separate record that will reference a document.
  • Thirteen new permissions, none implied by commerce.pos.create or by each other: commerce.held_sales.{read,create,update,approve}, commerce.quotations.{read,create,update}, commerce.quotation_conversions.create (a conversion also needs commerce.pos_due.create), commerce.work_orders.{read,create,update}, commerce.documents.{read,create}. Existing tenants do not gain them retroactively.
  • Backward compatible, opt-in. The whole surface is behind the new documents feature flag (commerce settings → Features), which defaults OFF: with it off every new route answers 409 FEATURE_DISABLED, the sidebar entry is hidden, and checkout, POS and the order APIs behave exactly as before (no existing table gained a column).
  • Hardening: awcms_app loses DELETE on all seven tables (and UPDATE on versions, events and documents); composite tenant-safe FKs; guard triggers freeze identity/provenance and allow only legal status moves; security-readiness.ts asserts the exact privilege sets.
  • Not here yet (ADR-0029 Deferred): an accounts-receivable invoice and credit notes, voiding or replacing a document, a receipt per payment, e-mail/WhatsApp/PDF delivery, a tenant-timezone numbering year, stock reservation by a quotation.

Loyalty points: an append-only ledger with earn, redeem, expiry, reversal and reconciliation (#289)

commerce gains a loyalty/rewards capability designed as an auditable points ledger, not a mutable points column (ADR-0026). The "why": the epic's rules (#281) are exact arithmetic, no destructive edits, payments and points as separate ledgers, idempotency on every high-risk write — so a balance is the sum of immutable facts, kept in the same transaction as each insert under a row lock, and every correction is a compensating row.

  • Migrations sql/950–952: awcms_commerce_loyalty_programs (versioned, effective-dated earn rules), _accounts (projected balance) and _ledger (append-only — awcms_app is revoked UPDATE/DELETE, a trigger rejects every UPDATE; per-tenant unique idempotency key; composite tenant-safe foreign keys), FORCE RLS, worker grants, four permissions.
  • Earn and reversal are driven by the order.paid / order.cancelled domain events (two new domain_event_runtime consumers), so no order, POS, pricing or payment-webhook code changed. Exactly once per order, from the order row's subtotal - discount in integer cents with FLOOR rounding, under the program version effective at paid_at. Walk-in customers never earn.
  • commerce:loyalty:expire (hourly, append-only, idempotent) and a read-only commerce:loyalty:reconcile (daily); an audited repair that rewrites only the projection.
  • Owner API (programs, accounts, ledger, redeem, adjust, summary, reconcile), a bearer-secured customer balance/history endpoint, and the /admin/commerce-loyalty screen. OpenAPI and AsyncAPI updated.
  • Behaviour change to know about: a sixth feature flag, features.loyalty, defaults to off — nothing changes for a tenant until it opts in.
  • Permissions are commerce.loyalty.{read,manage}, commerce.loyalty_adjustments.create and commerce.loyalty_redemptions.create, not loyalty.adjust|redeem, so the upstream-owned AccessAction union is not widened (a new subtree divergence).
  • awcms_worker gains SELECT on awcms_module_settings (the earn consumer reads the feature flag).
  • Deferred, with reasons in the ADR: redemption into checkout pricing/POS tender (needs #285), tiers, per-line return compensation (#287), campaign eligibility (#280), a storefront balance page.
  • Review hardening. Reconcile repair now rewrites only complete ledger histories (an account whose early rows were purged is reported unrepairable_history_purged), and an earn whose key belongs to another account is a recorded skipped_conflict instead of an endless retry.

Commerce payments become an append-only allocation ledger (issue #285, ADR-0025)

An order's payment was one column (payment_method) plus a `payment_status...

Read more

v0.14.0

Choose a tag to compare

@ahliweb ahliweb released this 28 Sep 18:17
f8a19b9

Shared translated label maps for commerce admin enums/statuses (issue #243)

A full sweep of every apps/cms/src/pages/admin/commerce*.astro screen plus
commerce-orders/[id].astro — every <option> text, table cell, status/tone
badge, detail field, and filter-tab label that shows an enum value — found
the same defect repeated across sixteen screens: a raw English/snake_case
value (order.status, session.provider, run.status, …) rendered straight
from the database instead of a translated label, each screen that needed one
either redefining its own ad hoc map or skipping it entirely, so the same
field (order status, e.g.) could read differently depending which of up to
four screens showed it. This change adds the one shared module the follow-up
screen changes adopt, so a status has exactly one Indonesian label everywhere
it appears.

  • New apps/cms/src/lib/ui/commerce-admin-labels.ts: createCommerceLabels(t)
    returns one object of 23 label maps — order status, payment status, order
    channel, product status, product type, voucher type and status, campaign
    status and channel, review status, affiliate status, commission status,
    conversation status, customer status, WhatsApp message status, flash-sale
    status, popup frequency, webhook-endpoint provider, payment-gateway session
    status/provider, payment-event outcome, and the shared reporting module's
    projection run/freshness status — each satisfies Record<TheEnumType, string> against the domain layer's own exported union, so a future new
    enum value is a compile error here until labelled. commerceLabel(map, raw) is the per-row lookup a screen calls; it never throws and falls back
    to the raw value. Six tone maps (orderStatusTone, affiliateStatusTone,
    commissionStatusTone, reviewStatusTone, reportFreshnessTone,
    reportRunStatusTone) are extracted from the STATUS_TONE/
    FRESHNESS_VARIANT maps that already exist, redundantly, on
    commerce-dashboard.astro, commerce-orders/[id].astro,
    commerce-affiliates.astro and commerce-reports.astro — no tone map for
    an enum no screen already colour-codes.
  • Nine of the 23 unions had no exported TS type anywhere: OrderChannel,
    PaymentGatewaySessionProvider and PaymentEventOutcome are derived from
    the owning column's own database CHECK constraint (sql/931, sql/926
    twice); ConversationStatus/AffiliateStatus/CommissionStatus/
    CustomerStatus are derived via indexed access into an already-exported
    record type (AdminConversationRecord["status"], etc.) rather than
    redeclared by hand.
  • locales/en.po/locales/id.po gain 26 new msgids under a
    # commerce admin labels (#243) comment. The 23 label maps use 54
    distinct msgids in total; the other 28 already existed — reused verbatim
    where an existing screen or apps/storefront's own
    order/affiliate/review/conversation labels already established the exact
    wording (e.g. commerce-affiliates.astro's own commission-status filter
    already spells "Pending"/"Approved"/"Paid"/"Void"; commerce-vouchers.astro,
    commerce-customers.astro and commerce-popup.astro's own <select>
    options already spell out every value their respective enum needs).
  • This issue ships no screen change — the sixteen screens the sweep
    found still render their own raw values today. Issues #245/#246/#247 adopt
    this module screen by screen; the adopting issues' own PR descriptions are
    the place to enumerate exactly which labels.<map> key replaces which
    file:line.

An accessible, fully-translated confirm dialog replaces window.confirm() on ten commerce admin screens

Part of the commerce admin v2 epic (LK admin v2 parity, issue #242). Eleven
window.confirm() calls across ten apps/cms commerce admin screens —
categories, campaigns (send + cancel), affiliates (approve/pay/void
commission), popup, products, reviews, sliders, testimonials, vouchers,
flash sales — are replaced by one shared, commerce-owned accessible dialog,
with every string an operator reads translated through t().

  • New apps/cms/src/components/CommerceConfirmDialog.astro: a native
    <dialog role="alertdialog">, rendered once per page, with
    aria-labelledby/aria-describedby wired to its own title/message
    regions, an optional (hidden-by-default) note <textarea> region for a
    future note-taking confirm, and Cancel/Confirm buttons reusing the
    existing .btn-secondary/.btn-primary/.btn-danger/.btn-danger--solid
    classes — no new button chrome invented.
  • New apps/cms/src/lib/ui/commerce-confirm-dialog-client.ts:
    confirmCommerceAction/confirmCommerceActionWithNote drive the dialog
    via showModal() (focus trap, Escape-to-cancel, backdrop, focus
    restoration to the opener all free), explicitly focusing the safe Cancel
    button first for a danger action. Falls back to window.confirm only
    when HTMLDialogElement/showModal() is unsupported or the dialog is
    missing from the page — the one remaining reference, documented as such.
    Pure option-normalization/note-trimming helpers are exported separately
    and unit-tested without a DOM.
  • No English literal ever reaches a screen's <script>. Every trigger
    button (delete/send/cancel/approve/pay/void) carries its own
    t()-translated data-confirm-title/-message/-label/-danger
    attributes, rendered server-side with any per-row value (a SKU, a name, a
    code) already interpolated via the catalogue's {placeholder} syntax. The
    new confirmFromTrigger(el) reads them back — the ONLY thing a screen's
    <script> calls; the pure readConfirmOptions(dataset) half is
    unit-tested without a DOM. Natural, professional Indonesian copy for all
    thirteen trigger actions is in id.po (e.g. "Hapus produk?" / "Produk
    {sku} akan dipindahkan ke sampah dan dapat dipulihkan nanti." / "Hapus
    produk"; "Kirim kampanye" is not danger, "Batalkan kampanye" is).
  • Affiliate commission approve/pay gained a confirmation they never had
    before ("Setujui komisi"/"Tandai dibayar", both non-danger) alongside
    void ("Batalkan komisi", danger) — a small UX addition, not a backend
    change, made possible by the same trigger now existing on all three
    buttons.
  • A campaign send stays non-danger (styled as the ordinary primary
    action); a campaign cancel, every affiliate void, and every delete action
    are danger (styled destructive, Cancel focused first).
  • No backend change: none of the endpoints these ten screens call accept or
    record a reason, so the note variant ships unused here — issue #246
    (order-status change) is the first real consumer.
  • apps/cms/scripts/client-asset-budget.ts's APP_BUDGET_BYTES raised
    259,000 B → 265,000 B for the new component/client module. Moving the
    confirm copy server-side ultimately measured LIGHTER than the first,
    literal-based pass (262,861 B vs. an interim 264,289 B), since every
    screen's own script shrank to a one-line confirmFromTrigger(button)
    call.
  • New test: apps/cms/tests/commerce-confirm-dialog.test.ts — pure helper
    coverage (including readConfirmOptions) plus a static contract over all
    ten screens: no window.confirm( or hand-built
    confirmCommerceAction({ title, message }) literal left, the component
    imported and rendered exactly once each, every trigger's data-confirm-*
    attributes present (checked per occurrence, since commission-void-btn
    renders twice), no inline style= in the component.

Commerce admin: translate stacked-table column labels

Issue #253. apps/cms/src/styles/admin.css renders each cell's column name
on stacked tables at 767px and below through content: attr(data-label).
Every commerce admin table but commerce-pos.astro (Issue #171's own screen)
set data-label="Status"/"Customer"/"Created"/etc. as bare English
literals, so a phone-width operator on an Indonesian admin saw untranslated
column names even though the table's own <th> header was correctly
translated with t().

  • 18 commerce admin screens (commerce.astro, commerce-affiliates.astro,
    commerce-campaigns.astro, commerce-categories.astro,
    commerce-customers.astro, commerce-dashboard.astro,
    commerce-flash-sales.astro, commerce-inbox.astro,
    commerce-orders.astro, commerce-orders/[id].astro,
    commerce-popup.astro, commerce-reports.astro,
    commerce-reviews.astro, commerce-settings.astro,
    commerce-sliders.astro, commerce-testimonials.astro,
    commerce-vouchers.astro, commerce-whatsapp.astro) now write
    data-label={t("…")} on every table cell, reusing the exact msgid the
    matching <th> in the same table already calls t() with — the stacked
    label always equals the column header now. commerce-pos.astro needed no
    change.
  • Two cells had no header text to copy directly:
    commerce.astro's row-checkbox column (header has no visible text) now
    reuses the existing "Select" msgid; commerce-affiliates.astro's
    commission table's first data column (header is a status-filter
    <select>, not a plain label) gets a new "Affiliate" msgid.
    commerce-affiliates.astro's affiliate-rate cell was also corrected from
    "Rate" to the header's own "Rate (%)" msgid, which it had silently
    drifted from.
  • New msgid: "Affiliate" (English + Indonesian "Afiliasi").
  • apps/cms/scripts/client-asset-budget.ts: no change to
    APP_BUDGET_BYTES — every cell already renders a translated string via
    the existing t() catalogue, so this adds no new script weight. The
    constant's docblock does record that this issue was checked against it.
  • Tests: new apps/cms/tests/commerce-data-label-i18n-253.test.ts — a
    repo-wide regression guard forbidding a literal data-label="…" on any
    commerce admin screen (present or future), plus targeted assertions for
    the "Select"/"Affiliate"/"Rate (%)" cases above.
    apps/cms/tests/commerce-settings-save-bar.test.ts (Issue #244) updated
    to expect t...
Read more

v0.13.1

Choose a tag to compare

@ahliweb ahliweb released this 27 Sep 06:33
03f1191

Apply Debian security updates in the storefront runtime image

apps/storefront/Dockerfile's runtime stage is FROM oven/bun:${BUN_VERSION}-slim, a
Debian base that only carries the package versions current the day that tag
was cut. A trivy scan of oven/bun:1.4.2-slim (27 Sep 2026, --severity CRITICAL) found three CRITICAL CVEs — CVE-2026-13221, CVE-2026-42496,
CVE-2026-8376 — all in perl-base 5.40.1-6, all fixed by Debian security in
5.40.1-6+deb13u1.

  • The runtime stage now runs apt-get update && apt-get upgrade -y --no-install-recommends && rm -rf /var/lib/apt/lists/* before USER bun,
    closing these (and future Debian-security fixes) on every rebuild rather
    than waiting on oven/bun to re-cut the base tag.
  • Only the runtime stage changes; deps and build are untouched.
  • docs/deployment.md/.id.md describe the updated runtime stage.
  • New test: tests/storefront-dockerfile-security-updates.test.mjs asserts
    the upgrade-and-cleanup RUN precedes USER bun.

v0.13.0

Choose a tag to compare

@ahliweb ahliweb released this 26 Sep 22:29
d1e1cd5

The primary nav is now actually visible above 720px

Header.astro (and NavBerita.astro on a berita-profile page) put the
primary navigation inside a <details>/<summary> disclosure and relied on
author CSS to show it as a plain horizontal nav above the mobile breakpoint.
That never worked: a closed <details> hides everything but its
<summary> through the UA stylesheet's own ::details-content rule, which
no CSS on the hidden content's children can override — so the primary nav
was invisible above 720px, in every browser, in all three SITE_PROFILEs
(issue #230).

The fix renders the same nav array twice — an always-open desktop
.primary-nav and the pre-existing mobile <details> copy — and lets
global.css show exactly one of the two by viewport width, display: none-ing the other out of the accessibility tree so a screen reader still
meets exactly one "Navigasi utama" landmark. No JavaScript is involved,
keeping issue #24's no-JS mobile-nav design intact.

  • A reader on any deployment of this template can now reach every primary
    nav link at desktop width — previously, only the mobile disclosure ever
    worked.
  • apps/storefront/tests/e2e/navigasi-utama.e2e.ts is new regression
    coverage: at 1440px it asserts the desktop nav is visible and
    keyboard-focusable, and that exactly one "Navigasi utama" landmark is
    accessible at a time; at 360px it asserts the desktop copy is hidden and
    the disclosure toggle still opens the mobile nav. Run for all three
    build profiles.

Local/server CI runner with exact-SHA commit statuses (#225 part 1)

The owner decided to move this workspace's CI off GitHub Actions entirely. This is part 1: tools/ci/ reproduces every leg .github/workflows/{ci,template-init-smoke,e2e,codeql}.yml runs today, one-to-one, as twelve local-ci/* commit-status contexts reported against the exact SHA each leg ran at — see ADR-0021.

  • bun run ci runs all twelve legs against HEAD inside a disposable git worktree, never mutating the developer's own checkout; bun run ci:pr -- <n> does the same for a pull request, refusing fork PRs unless --allow-fork is given explicitly.
  • bun run ci:watch polls open PRs and skips any (repo, PR, head SHA, CI-definition version) already recorded; tools/ci/systemd/ documents the systemd user timer that runs it every 10 minutes (not installed by this change).
  • Every leg fails closed if the running Bun does not match the root package.json's pin; every log/evidence file is redacted for tokens and DSN passwords before it touches disk.
  • The local-ci/security leg runs the CodeQL CLI, gitleaks (pinned by digest), and bun audit, checked against a committed tools/ci/security-baseline.json seeded with this repository's nine already-dismissed GitHub code-scanning alerts (quoting each one's own dismissal reason) plus one further entry for a genuine new-code finding this leg's own first run surfaced in tools/ci/lib/lock.ts.
  • .github/workflows/*.yml are not removed by this change — see the ADR's "Migration sequence" for why branch protection moves to local-ci/* first, in a follow-up PR.

Local CI runs every leg with CI=true, as GitHub Actions did

GitHub Actions set CI=true for every step, and both Playwright configs key
their CI behaviour off it. Without it, the local e2e legs silently ran
without forbidOnly (a stray test.only would have skipped the rest of the
suite and still passed), without the one retry for a flaky
Page.captureScreenshot protocol error, and without the HTML report the leg
keeps as evidence. tools/ci/lib/orchestrate.ts now sets it once, after the
Bun pin check, for every leg.

Build, sign, and publish release images and GitHub Releases from a trusted release host

Issue #225 part 2 (zero-GitHub-Actions policy). tools/release/images.ts and tools/release/publish.ts (bun run release:images, bun run release:publish) replace what .github/workflows/images.yml and .github/workflows/release.yml do — building and pushing apps/cms's runtime/jobs images to GHCR, signing each pushed digest with cosign, scanning with trivy (fail-closed on CRITICAL), exporting SBOMs, and publishing the matching GitHub Release — from a release host awcms-one itself controls, rather than a GitHub-hosted runner.

Neither workflow file is removed by this change; both keep running until a later change disables GitHub Actions at the repository level and migrates the required-status-check contexts (issue #225's own remaining steps).

  • ADR-0023 documents the new cosign-key trust root, why it supersedes ADR-0020's GitHub-native attestation for images published from here on, and the honest assurance-level trade-off.
  • docs/rilis.md is the full release runbook, including how to rehearse the whole publish path against a throwaway local registry without touching GHCR or a production key.
  • New root env vars: GHCR_USER, GHCR_TOKEN, COSIGN_KEY, COSIGN_PASSWORD, COSIGN_PUBLIC_KEY, RELEASE_EVIDENCE_DIR, RELEASE_DANGEROUSLY_SKIP_ANCESTOR_CHECK, RELEASE_GITHUB_TOKEN — all documented in root .env.example.
  • An owner running the first real --publish needs a COSIGN_KEY/COSIGN_PUBLIC_KEY pair provisioned on the release host and a GHCR_TOKEN scoped to write:packages only — neither exists yet as a repository secret, by design (this tool never generates a signing key on the fly).

Remove GitHub Actions; required contexts are now local-ci/* (#225 part 3, closes #225)

The owner's zero-GitHub-Actions decision (ADR-0021), started in part 1 (tools/ci/) and part 2 (tools/release/, ADR-0023), is now complete: every root workflow is deleted and GitHub Actions no longer executes anything in this repository.

  • Removed .github/workflows/{ci,codeql,e2e,images,release,template-init-smoke}.yml and .github/dependabot.yml (its only ecosystem, github-actions, is now meaningless).
  • Added tests/tanpa-github-actions.test.mjs — a root gate test that fails if a workflow file ever exists again under root .github/workflows/, and explicitly excludes apps/cms/.github/** (upstream's own subtree files, inert here since GitHub only reads a repository's own root .github/workflows/). Subsumes issue #224's workflow-guardrail requirement.
  • Fixed every test/tool that read a now-deleted workflow file: tests/versi-toolchain.test.mjs now checks tools/ci/lib/orchestrate.ts's Bun-pin enforcement instead of a bun-version: line; the test that guarded .github/dependabot.yml's own shape is removed with the config file it guarded; several tools/tests updated their comments to point at the tools/ci//tools/release/ successors instead of claiming current GitHub Actions behaviour (artifact uploads, automatic tag-push publication).
  • docs/template.md confirms and documents that tools/template-init/** never generates or references workflows — a derived repository starts with zero root GitHub Actions workflows too, and runs bun run ci/ci:watch locally or attaches its own approved CI by its own governance decision.
  • Full documentation sweep (EN + ID mirrors): AGENTS.md (new explicit rule that GitHub Actions is not an accepted implementation path; the gates table now lists the twelve local-ci/* legs; the Bun-pin/Dependabot paragraphs rewritten; Definition of Done requires bun run ci:pr -- <n>), README.md, SECURITY.md, docs/alur-kerja-pengembangan.md (five old per-workflow CI sections folded into one "Local CI: the twelve legs" section, keeping each leg's promotion history for the record), docs/pengujian.md, docs/deployment.md, docs/status.md, docs/template.md, docs/aksesibilitas.md, docs/responsif.md, docs/routing.md. ADR-0018 and ADR-0020 keep their original decisions; each gets a short "Superseded in part by ADR-0021/0023" note (their own CI/publish mechanism moved, not the decision itself), and the ADR index status column reflects it.
  • The twelve required status checks on main are now local-ci/* (posted by bun run ci:pr/ci:watch) instead of the old GitHub Actions contexts — branch protection was switched over in a companion change before this PR merged, so no Actions checks ever needed to run on it.

Server-side, explicit production deployment (tools/deploy/*.sh, ADR-0022)

GitHub Actions could, until now, plausibly grow into the production control
plane by accident — nothing in this repository forbade a future workflow
from SSHing into production or calling a Coolify deploy API with
production credentials in its secrets, and a validated derived repository
already has exactly that shape today. This closes that gap: the ONE
canonical way to mutate production is now tools/deploy/deploy-production.sh
(plus its thin deploy-remote.sh SSH wrapper, healthcheck-production.sh,
and rollback-production.sh), invoked explicitly by an operator or agent,
never by CI.

  • New root scripts: bun run deploy:production|remote|health|rollback.
    Fail-closed transaction (lock, exact-release resolution, clean-checkout
    check, preflight, pre-migration backup, build-or-pull, migrate, runtime
    least-privilege verification, activate, health, smoke, append-only audit
    record); a migration having run in the failed attempt blocks any automatic
    code rollback and points at the documented DB recovery runbook instead.
  • New packages/gerbang/lib/redact.mjs + tools/deploy/redact-log.mjs:
    every line these scripts print, and the audit JSONL itself, is redacted
    before it reaches a terminal or disk.
  • tests/deploy-production.test.mjs — a hermetic scenario suite; no real
    docker/git/curl/ssh/cosign is ever invoked.
  • docs/adr/0022-production-deployment-is-server-side-and-explicit.md +
    .id.md; docs/deployment.md/.id.md rewritten to separate CI checks,
    art...
Read more

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 24 Sep 07:29
3606ac6

Align the root-owned Bun pin to 1.4.2

The #210 subtree sync left a root-owned toolchain drift: root package.json
still pinned packageManager: "bun@1.4.0"/engines.bun: ">=1.3.0" while the
embedded apps/cms/package.json (a leftover from before the subtree embed,
not part of this pin) already read bun@1.4.2, and every root workflow that
installs Bun (ci.yml, template-init-smoke.yml, e2e.yml, images.yml,
release.yml) still requested 1.4.0. Two Bun patch versions coexisting in
one monorepo makes local/CI reproduction less deterministic.

  • Raised the authoritative root pin to bun@1.4.2: package.json's
    packageManager, and bun-version: "1.4.2" in every job of every root
    workflow above.
  • engines.bun moved from >=1.3.0 to >=1.4.2 — its meaning stays a floor,
    not an exact pin (it also gates apps/storefront's own bun install and
    any contributor's local Bun, where "at least this new enough" is the
    actual requirement), only its value moved up alongside the rest.
  • Corrected AGENTS.md's (and its Indonesian mirror's) "Configuration and
    toolchain" bullet, which had stated the pin as living in "three places"
    naming only ci.yml — already an undercount before this change, since
    release.yml's own bun-version job existed and was not named. The rule
    now names the actual, current set of root workflows that install Bun
    instead of a fixed count, and a workflow that starts installing Bun in the
    future is expected to join that list in the same change that adds its
    bun-version line.
  • No lockfile content changed: this repository's local Bun runtime is
    already 1.4.2, so bun install and bun run check:lockfile were exercised
    under that same version and bun.lock came back byte-identical.

Astro/Playwright/Changesets updates that arrived through #210 remain that
subtree sync's own scope and are untouched here; this change is root-owned
toolchain alignment only (issue #211).

Encrypted backup assurance and restore drills, integrated into the production topology

compose.production.yaml gains three profile-gated services — backup, restore-drill, offsite-copy — that reuse upstream apps/cms/deploy/backup/*.sh (issue #210, ADR-0123) through this repository's own distinct awcms_setup/awcms_app/awcms_worker identities, rather than forking or reimplementing that tooling. docker/backup/Dockerfile supplies only the binaries (age, rsync, openssh-client) those scripts' own README already says a runner image needs, on top of the same postgres:18.4 the postgres service runs. ops/run-backup-compose.sh and ops/awcms-one-backup.crontab give the host-cron scheduling path.

  • Backups are age-encrypted with an HMAC-SHA256-authenticated manifest; a local-only backup is never reported as a successful off-site copy.
  • restore-drill has a hard-coded command with no --target code path anywhere in it — structurally incapable of targeting a production database, not merely documented as a drill. A real disaster-recovery restore stays a manual, confirmed runbook step, never a compose service.
  • Validated end to end against a disposable PostgreSQL with synthetic data: create → backup → encrypt → manifest → off-site copy (a disposable SSH test target) → restore-drill into an isolated scratch database → schema/RLS integrity verification → measured RTO/RPO, plus five fail-closed paths (wrong/missing key material, tampered manifest, corrupted backup, unavailable off-site destination, unsafe restore target).
  • docs/deployment.md's new "Backup assurance" section (and its Indonesian mirror) is the operator runbook; docs/status.md removes its "at-rest backup encryption" gap now that the capability is documented and validated.

Test awcms-one's deployment/migration boundary around the synchronized omes_control module

Issue #210 pulled upstream's omes_control domain module into apps/cms
(sql/154-sql/158, awcms's own ADR-0122). Upstream already tests that module's own unit
and application semantics; issue #212 adds the AWCMS-One-specific integration
boundary around it — deployment/migration shape this repo owns, not a re-test
of upstream's own coverage, and no OMES/Hermes runtime-execution behaviour
(issue #146 moved that ownership to ahliweb/omes on purpose).

  • apps/cms/tests/integration/omes-control-deployment-boundary.integration.test.ts
    (12 tests, real PostgreSQL): 154-158 and the full commerce 901-934
    range apply to one clean database with no numeric-prefix collision and a
    migration ledger that matches the files on disk (ADR-0015's reserved-range
    rule); awcms_setup/awcms_app/awcms_worker are three distinct roles on
    a migrated cluster, matching compose.production.yaml's three DSNs; all
    eight omes_control tables carry RLS both ENABLEd and FORCEd;
    cross-tenant reads/writes on omes_control data fail closed; a freshly
    migrated tenant holds zero omes_control role-permission grants
    (default-deny survives deployment); existing commerce RLS/tenant isolation
    stays green alongside the new module; and, as the required negative
    test
    , deliberately re-granting awcms_worker the exact INSERT/UPDATE
    privileges sql/156 revoked is caught by the least-privilege assertion
    (with an over-grant message naming the table and both verbs), then reverted
    and re-verified passing.
  • apps/cms/tests/omes-control-execution-boundary.test.ts (DB-free, 2 tests):
    no source file under src/modules/omes-control/ or
    src/pages/api/v1/omes/ imports child_process/ssh2, calls
    Bun.spawn, or calls a raw exec/execSync — the module stays a
    tenant-scoped control-plane record store, never a shell/SSH executor.
  • Both suites run in the existing DB-backed check-cms CI leg (apps/cms's
    own bun test tests/integration/ step and its plain bun test
    respectively) — no second migration/test pipeline was added, and root
    bun test remains PostgreSQL-free.
  • docs/pengujian.md/.id.md document the new coverage under check-cms's
    section.

Promote CodeQL to a required pull-request status check

CodeQL (issue #184) ran on every push and PR for a full triage cycle
(issue #206) with a green track record and no un-triaged findings — the
probation condition set when it was introduced by PR #194. That
prerequisite is now satisfied, so it is promoted from advisory to a
required merge gate on main (issue #214).

  • main's branch protection now requires Analyze (javascript-typescript)
    — the CodeQL workflow's own job-status check (app: GitHub Actions) —
    additively, alongside the eleven contexts issue #215 had already
    established. All eleven survive unchanged; enforce_admins and strict
    are untouched.
  • Not the sibling CodeQL check (app: GitHub Advanced Security, the
    code-scanning-results check posted only once a SARIF upload succeeds):
    verified from a real PR's check-runs that the job-status check is the
    one that fails closed on a workflow/analyzer failure, which is what a
    required security gate needs — the results check cannot represent an
    analyzer crash that never got far enough to upload a SARIF at all.
  • security-extended, apps/cms/** in scope, the weekly schedule,
    SHA-pinned actions, and least-privilege workflow permissions are all
    unchanged. security-and-quality is deliberately not made
    merge-blocking — a separate signal/risk decision, out of scope here.
  • Documentation and workflow comments that described CodeQL as "not
    required" are updated: AGENTS.md/AGENTS.id.md, SECURITY.md/
    SECURITY.id.md, README.md/README.id.md, docs/status.md/
    docs/status.id.md, docs/alur-kerja-pengembangan.md/.id.md, and
    .github/workflows/codeql.yml's own comments.

Promote the Playwright e2e matrix to required status checks

.github/workflows/e2e.yml (issue #183) was introduced on a deliberate
probation period: a real-browser suite carries a different flake risk than
a type-check or unit test, so it started as advisory only, pending a
proven, deterministic run history.

That bar is now met. Issue #215 reviewed the post-introduction run history
(gh run list --workflow e2e.yml) and found at least seven consecutive
green runs across #197/#202/#203/#204/#207/#208/#209, with no observed e2e
failure in any run since the workflow was introduced. The exact
e2e (toko)/e2e (berita)/e2e (landing) contexts were verified against
a real PR's checks (gh pr checks, gh api .../check-runs) rather than
inferred from the workflow file, and all three are now added to main's
branch protection additively — every previously required check
(check-cms, the three Check (*) legs, and the four
template-init-smoke legs) is preserved, and enforce_admins stays on.

  • main's required status checks go from eight contexts to eleven.
  • A pending or failing e2e (toko)/e2e (berita)/e2e (landing) leg now
    blocks a PR merge, the same as any other required check.
  • No test scope, assertion, or screenshot behaviour changed — this is a
    branch-protection and documentation change only.
  • .github/workflows/e2e.yml's own comment, AGENTS.md, README.md,
    docs/status.md, and docs/alur-kerja-pengembangan.md/docs/pengujian.md
    (plus their Indonesian mirrors) no longer describe this workflow as
    optional.

Sync the apps/cms subtree from upstream 8c64528d to 2d29a446

apps/cms is ahliweb/awcms embedded via git subtree (see AGENTS.md's "The
subtree embed"). This pulls the 14 upstream commits that landed since the last
sync (issue #170), full-history, with a merge commit (issue #210).

  • The omes_control domain module (26th registered module) — host fleet
    lifecycle, worker enrollments, desired-vs-observed deployments, an
    allowlisted safe-operation surface, a worker job dispatch queue, health/
    backup/audit projections — arrives in the embedded tree via migrations
    sql/154-sql/158. Its worker grant ...
Read more

v0.11.1

Choose a tag to compare

@github-actions github-actions released this 23 Sep 22:38
021fd5d

First CodeQL triage: dismiss the false positives, record the standing rules

Thirteen of the fifteen CodeQL alerts open on main after issue #184 were false
positives, won't-fix, or test-only findings, each verified against the actual
flagged line before being dismissed via the code-scanning API with a reason —
not accepted or rejected on the tool's word alone.

  • Dismissed: js/insufficient-password-hash ×3 (a SHA-256 hash over a
    randomBytes(32) token is not a password hash; computePkceChallengeS256 is
    RFC 7636 §4.2, which mandates SHA-256), js/file-access-to-http ×4 and one
    js/clear-text-logging (tools/seed-cms.ts's own purpose — reading local
    seed data and printing a one-time generated password for its operator),
    js/file-system-race ×3 (single-user local tooling, or test-only), and one
    each of js/reflected-xss and js/incomplete-url-substring-sanitization
    (both test-only, not production code paths).
  • Left open, on purpose: two js/clear-text-logging alerts on
    apps/cms/scripts/commerce-deploy-preflight.ts — real findings, fixed as
    code by a sibling change rather than dismissed here.
  • SECURITY.md gains a "CodeQL triage" section naming where alerts appear, why
    security-extended stays the query suite, how an apps/cms/** alert is
    triaged (upstream's tree vs. this repo's own commerce module), the
    dismissed rule IDs, and the two standing rules above so a future reader does
    not re-litigate them. docs/alur-kerja-pengembangan.md's codeql workflow
    section now cross-links it instead of stating a since-outdated "no triage
    playbook yet".

Both changed governance documents ship their Indonesian mirror in this same
change (bun run docs:i18n:stamp).

commerce:deploy:preflight redacts credential-shaped values before printing

CodeQL's first triage after the production-preflight work (#184) flagged two
js/clear-text-logging alerts on apps/cms/scripts/commerce-deploy-preflight.ts:
the two paths that echo a delegated script's captured child-process stderr
print a value CodeQL taints back to process.env. Neither path was actually
printing a credential today, but the property was only true by hand-checked
convention in a script whose output runs through deploy pipelines with
retained logs.

  • A single exported redact() helper now masks a postgres://user:pass@host
    DSN's password, a bearer/API-token-shaped value, and the whole value of any
    env variable whose NAME matches /(PASSWORD|SECRET|TOKEN|KEY|DSN|DATABASE_URL)/i.
  • Applied to every string the script actually prints — printResult, the
    main() startup banner, and both captured child-process stderr/stdout
    tails — so nothing bypasses it. A benign value (a provider name, a role
    name, a plain URL) passes through unchanged, unit-tested alongside the
    three masked shapes in apps/cms/tests/commerce-deploy-preflight.test.ts.
  • docs/deployment.md (+ its Indonesian mirror) now says what the preflight
    prints and what it redacts.