Skip to main content
Version: v5.6.1

OpsChain GA Release 5.6.1

OpsChain 5.6.1 is a generally available release, published on 2026-11-09. Every release is listed on the releases index.

Added​

  • What references an installed pack version is visible before a delete refuses: the v2 PackVersion carries referenced_by — the template versions whose sealed set pins the version (project_code, template_code, template_id, template_version, template_version_id, so the GUI links each one) — and run_count, the runs whose frozen set pins it; both read the same pack-pin rows the delete refusal counts, filled in one set-based query per listing page (and on the install and pin responses), and the refusal now derives from that same read. The v1 template version gains sealed_packs ([{name, version}], always an array) — the installed pack versions its pinned set sealed — so a template version's page shows the relation from its side. opschain admin packs list gains the REFERENCED BY column (<n> tv / <m> runs, filterable as referenced_by) and -o json carries referenced_by and run_count. Reference: opschain docs pack-store, resources/pack (the new PackVersionReference), resources/template, cli/admin-packs-list; lesson 2.4. The Packs admin page and the template version page render them in the GUI half of the same train.
  • The CLI references shared git credentials: opschain git-remotes create/update gain --credential-ref <id|name> — the remote points at an entry of the shared git-credential registry (the project's own or a global one) instead of carrying inline values, so rotating the credential once applies to every referencing remote on its next fetch; the flag is refused locally beside any inline credential flag (--credential-ref cannot be combined with inline credentials … — supply one or the other), the server's own refuse-both wording passes through, --credential-ref "" on update clears the reference and may accompany the inline values that replace it, and the strict -f file accepts credential_ref (a reference, not a secret — the secret keys stay refused). show prints credential_ref and list's AUTH column reads shared-credential. A new opschain git-credentials family manages the registry over the v2 GitCredentialService: list [--project] (a project's own plus the global credentials, or the global registry alone; masked values print as the API returns them, {{hidden}}), create --name --kind https_basic|ssh_key [--project] with the git-remotes credential contract (--username, --password-env/--password-prompt, --ssh-key-file, --passphrase-env — never a secret on argv or in a file), update <id|name> (send-only-what-is-supplied; --username ""/--clear-passphrase clear), test <id|name> --url (the server's git ls-remote probe; a failed probe exits 1 with the classified reason) and delete <id|name> --confirm (preview without it; the in-use refusal names the remotes). Reference: opschain docs cli/git-credentials (+ -list/-create/-update/-test/-delete), cli/git-remotes-create, cli/git-remotes-update, resources/git_credential; lesson 3.3 teaches the reference form as the recommended one.
  • Session origin in the audit trail — the proxy-aware client address, the user agent and the acting credential on every audited event and session, with a stated retention and privacy posture: (1) server.trusted_proxies (config.yaml, a CIDR list; default empty = the TCP peer is the client) names the reverse proxies whose X-Forwarded-For / CF-Connecting-IP the server honours — when the peer is listed the client is CF-Connecting-IP if present, else the right-most X-Forwarded-For hop that is not itself listed; an unlisted peer's headers are ignored, so a direct caller can never spoof its recorded address; an invalid CIDR refuses server start by name and the validator reports the same. One resolver (internal/server/requestorigin) runs once per request in the request-log middleware and feeds the log's remote_ip, the per-source login throttle, the session row and the audit envelope, so all four agree. (2) Every event a request causes — the api:* mutation audit, the api:auth:*/warn:auth:* family, the server-emitted lifecycle events (node delete/restore/destroy, maintenance, licence, template-version, actions-refresh, artefact creation) — carries an actor object in data: principal_id, session_id (native session) or token_id + token_kind, client_address, user_agent, request_id, each present only when known, never a username or email; absent on engine-originated events; an emitter's own actor is never overwritten; it rides data verbatim into the SIEM forwarder. The api:auth:*/warn:auth:*/MFA/password family carries its client address ONLY as actor.client_address — the legacy data.source key is gone (owner 2026-11-08: pre-GA, no consumers), so a SIEM rule on a login's origin keys on the envelope. (3) auth_sessions gains user_agent (first seen, never rewritten) and last_client_address (updated on every refresh) — migration 00126_auth_sessions_origin (expand) — exposed on the v2 Session resource (user_agent, last_client_address) and opschain admin sessions list (LAST ADDRESS and AGENT columns; JSON last_client_address, user_agent), so a user recognises their own devices. (4) The retention and privacy posture is stated: opschain docs security-audit ("What the audit trail holds about origin") tables what is held where and for how long — retention.events_days (events, default never), session purge, logging.max_age_days (the request log, 30 days) — with no hashing of the address (the forensic value is the point; retention is the proportionality control). Reference: opschain docs events (the audit envelope), resources/event, resources/session, config (server:), identity, cli/admin-sessions-list; runbook 7-3.

Changed​

  • The release keyring carries production keys only: pkg/licence/keyring.json holds 2026-a (issuing) and 2026-registration-a (registration) alone; the 2026-test and 2026-registration-test entries — whose private halves are committed test data — move to pkg/licence/keyring_dev.json, embedded only under -tags devkeys and appended to the release keyring at first use (a duplicate id refuses the build's keyring). make build/dev-bin, make demo-bin, make check (go test -tags devkeys ./... -race, lint with the tag) and the integration test binaries carry the dev ring; make release and the train never do, and opschain version prints dev keys: on exactly on the builds that carry it. Every 2026-test-signed licence (examples/licences/*, the demo kit's, the integration harness's) therefore verifies on devkeys builds only — a released binary answers licence: unknown signing key — so the published licence asset becomes opschain-evaluation.licence (replacing opschain-dev-full.licence): the production-signed, 90-day, every-feature licence the owner mints with 2026-a before the cut and hands the train as RELEASE_LICENCE. The train's pre-flight (make release-licence-check, scripts/release/check-licence.sh) verifies it offline against the release keyring (go run ./scripts/release/verify-licence, untagged) and refuses by name when it is unset or signed by a key the release keyring does not hold; the cut's own suite walk (3c′, which now finds a cached opschain-evaluation.licence), the release-artifact pass (6, OPSCHAIN_IT_LICENCE — the integration harness refuses a release binary without one) and the publish step (7, re-verified; a GA tag refuses without it, an internal publish re-ships the previous release's re-verified) all apply this one licence; the release_train workflow gains the licence input. opschain admin packs install now refuses a bundle signed by an unknown key naming it: unknown signing key "<key_id>": the bundle is signed with a key this build does not trust. Docs: the licensing runbook, README.release-train.md (precondition 4b, steps 3c′/7), examples/licences/README.md, the entitlements and version/admin packs install reference pages, docs/cli.md.
  • The hosted platform services live on limepoint.com: licensing.limepoint.cloud, licensing-admin.limepoint.cloud, academy.limepoint.cloud and academy-admin.limepoint.cloud become licensing.limepoint.com, licensing-admin.limepoint.com, academy.limepoint.com and academy-admin.limepoint.com — same labels, new apex, because customers' egress allow-lists already admit .limepoint.com and the zone is already on Cloudflare. The Academy launcher's default server (DefaultServer, the embedded release.json), the academy manuals and lesson 0.3 carry the new name; nothing was ever deployed on the .cloud hosts, so there is no redirect bridge — the next binary simply carries the new default.

Added​

  • The release-lane keyring gates: make release-keyring-check (a make release prerequisite) runs the release-lane tests untagged, exactly as the release binary builds — TestReleaseKeyringCarriesNoTestKey / TestKeyringFileCarriesNoTestKey (no -test id, no development role, at least one issuing key; the dev licence a stranger) and TestBundledPackManifestSignerIsInTheReleaseKeyring. make release-publish-selftest (in release-distribution-selftest) exercises the publish step's licence rule offline with a stubbed gh and the real verifier — the test-key-signed dev licence as the stranger; RELEASE_LICENCE_PROOF=<file> runs the acceptance path against the owner's licence, read in place. Proven 2026-11-08 with the owner's opschain-evaluation.licence (id 5c1d7b5d…, signer 2026-a): the verifier, check-licence.sh and the self-test's acceptance path all green.
  • The public releases repository and the Homebrew tap carry the LimePoint LICENCE notice (owner, 2026-11-08): the OpsChain End User Licence Agreement governs the published packages and formulae; the publisher (step 7b) and the tap push (7c) write it from scripts/release/public-repo/LICENCE on every cut, and rule 2's tree check admits it beside README.md, CHANGELOG.md and install.sh.

Fixed​

  • Every local run and every template resolution failed from a directory with a space in its path (macOS: ~/Library/Application Support/opschain/work/…, the default home for local runs): the embedded JSON schemas were registered under a bare file name the compiler resolved against the working directory, percent-encoded differently on lookup, and so "failed loading" from disk — opschain actions list --local refused, and after the server reported every action file invalid and refused change creation (embedded schema actionfile_schema.json does not compile). The schemas are registered under an absolute synthetic URL; nothing is loaded from disk. Found by the v5.6.1 cut's own suites walk.
  • An optional secret input left unset is absent, not an empty raw value the engine refuses: a secret input's absence is now the empty string — a secret cannot declare default: '', so an optional type: secret input left unset binds "" (where it used to bind nothing and fail no such key when a workflow step referenced it), an empty string supplied for one means "not supplied" (the declared default applies, or it binds ""; a required secret left empty is still input <name> is required and was not supplied), ${{ inputs.x }} renders empty for it and inputs.x != '' tests whether it was given; a non-empty value that is not a secret-vault:// reference is refused exactly as before. The run record and the stored inputs show "" for an unsupplied secret, never ******. The rule applies wherever inputs bind: change and workflow-run creation (v1/v2 API, the plan lane), a workflow step's with: pass-through into its nested change (the hosted-provision case — with: { x: ${{ inputs.x }} } for optional secrets the caller did not set ended the step error at its entry with no reason on the step), a composite action's with: literal, opschain runs create … -i secret_input= and the local lane. Reference: opschain docs orchestration-logic, expressions, orchestration, cli/runs-create; lesson 3.5.
  • Why a workflow step failed reaches the operator, not only the server log: the first hosted-provision run ended system_error on provision-site with nothing on the step — the action file's validation error (output key "output_originIp" must match …) lived in the server log alone. Every step now exposes last_error_message, the reason recorded at its last error/system_error, verbatim: on the v1 step resource and the workflow step's attribute document, on the v2 Step (last_error_message), in opschain runs show (printed under the failed step as ↳ <reason>; -o json|yaml carry the field) and on the GUI step tree (the GUI half of the same train). For a change step whose nested change is refused at creation — an action whose declarative file is invalid, a with: naming an input the action does not declare, a missing required input, an action absent from the catalog — the reason is the API's own refusal, exactly the text POST /api/changes would answer with. A broken action file is also visible before any run: resolving a template version's action catalog records its invalid files on the version (invalid_action_files: [{file, action, error}], new on the v1 template version; migration 00127_template_version_invalid_action_files, expand) and emits warn:template:action_file_invalid (source: the template version; data: file, action, error) once per template version and file — never per run or per catalog read, a changed error warns again. Reference: opschain docs resources/step, resources/workflow_run, resources/template, events, cli/runs-show; lesson 4.3.
  • A pack bundle's min_platform floor no longer refuses a development build: a git describe past a tag (v5.6.0-26-g…) or a dirty build is never placed against the floor, which protects released estates only.
  • The release lane's self-tests stage under their own root: the publish-public, package-managers, sign-release and write-manifest self-tests used v5.6.0 as their fixture version and read — then deleted on cleanup — the repository's real dist/release/v5.6.0 stage, taking the signing facts the hosted lane had just recorded with it; on the first GA cut that failed one check ("before a signed release the README says the Windows fingerprint is pending" — the real facts were there) and removed the stage the next steps needed. The four scripts take RELEASE_DIST_ROOT (default dist/release) and every self-test exports its own root under its temp dir; the whole release-distribution-selftest set now leaves a real stage byte-identical (checked on the cut).