opentide deploy
Deploy the changed rules whose status maps to a strategy, to the tenants that accept it.
Deploy the rules a run changed whose status maps to the run's --strategy, to the tenants that accept that strategy.
opentide deploy --strategy PREVIEW --dry-run # pull or merge request job
opentide deploy --strategy RELEASE --platform splunk # default-branch job
opentide deploy --strategy PREVIEW --strategy RELEASE --all --dry-run # every rule fileThree things decide what is sent:
- The rule's status maps to a strategy in
deployment.toml. With the default statuses,DEVELOPMENT,IMPROVING,STAGINGandACCEPTANCEmap toPREVIEW, andPRODUCTIONmaps toRELEASE. Status names never matter, only the strategy they map to. - The run deploys a strategy.
--strategy PREVIEWdeploys rules mapping toPREVIEWorUNIVERSAL.--strategy RELEASEdeploys rules mapping toRELEASEorUNIVERSAL, and disables or deletes rules mapping toDISABLEMENTorDELETION. - The tenant accepts the strategy. A tenant's
strategies(default: both) decide which runs reach it.
Statuses, strategies and tenants walks through this with examples.
When to use it
- In a pull or merge request pipeline, with
--strategy PREVIEW, to deploy the changed rules to the tenants that take previews. - On merge to the default branch, with
--strategy RELEASE, to deploy, disable or delete the changed rules on the tenants that take releases. - Locally with
--dry-runto preview what a deploy would send, and to which tenants.
Always --dry-run first, and always validate before deploying.
Prerequisites
A real (non-dry-run) deploy contacts the platform API, so it needs:
- The platform enabled in
.opentide/configurations/platforms/<name>.toml. - Credentials for that platform, typically via environment variables referenced from the platform TOML — see Configuration → credentials.
--dry-run needs neither and is safe to run anywhere.
Options
| Flag | Purpose |
|---|---|
--strategy | Required, repeatable. PREVIEW: rules whose status maps to PREVIEW or UNIVERSAL. RELEASE: rules whose status maps to RELEASE or UNIVERSAL; also disables DISABLEMENT rules and deletes DELETION rules. Each rule goes to the tenants whose strategies accept the run's strategy. A status name such as STAGING is refused, and the message names the strategy it maps to |
--all | Read every rule file. Without it, deploy reads the rule files the run changed: the pull or merge request (from its merge base), the push, or outside CI the last commit |
--platform | Target platform (see platforms) |
--wide | Deploy the rule files read whatever their status strategy, INERT included, to the tenants accepting the run's strategies. DISABLEMENT and DELETION still follow --keep-deprecated |
--dry-run | Simulate deployment without writes. --json lists the tenants each rule would reach |
--keep-deprecated / --drop-deprecated | Include rules whose status maps to DISABLEMENT or DELETION, so a RELEASE run disables or deletes them. Default: on, off with --all |
--skip-promotion | Skip the promotion step of a RELEASE run on a push |
--skip-unconfigured | Exit 0 when every platform with rules to deploy has no [[tenants]] block |
--persist-state-externally | In CI, allow a run other than --strategy RELEASE on a push to create Defender for Endpoint, SentinelOne and CrowdStrike rules. Pass it only when the pipeline commits .opentide/states/deployment/ itself; see Creates outside the production job |
--plan and DEPLOYMENT_PLAN were removed in 0.17.0. Either now fails with exit 2 and names the --strategy to pass: STAGING became --strategy PREVIEW, PRODUCTION became --strategy RELEASE, and FULL became --strategy PREVIEW --strategy RELEASE --all. A platform file whose tenant still sets deployment fails to load and names the strategies or manual line that replaces it (migration table).
Output
A dry-run in the tutorial repository prints the plan: each rule on each tenant with what a deploy would do (+ create, ~ update, + create or update, - disable, × delete), then the tally as the outcome line:
opentide deploy --platform sentinel --dry-run --strategy PREVIEW --allDeploy plan · strategy PREVIEW · 1 platform, 0 tenants · dry run, nothing is sent
sentinel · 0 tenants
+ create or update Sentinel KQL Rule …000000000001 status STAGING
warning: sentinel has no tenants configured in .opentide/configurations/platforms/sentinel.toml, so a real deploy would stop
Plan: 0 to create, 0 to update, 1 to create or update, 0 to disable, 0 to delete
help: add (or uncomment) a [[tenants]] entry in .opentide/configurations/platforms/sentinel.tomlDefender for Endpoint, SentinelOne and CrowdStrike keep vendor ids in .opentide/states/deployment/, so their rows say create or update. The other platforms write a rule by its UUID whether it exists or not, and opentide does not claim either: their rows say create or update. A real run ends with a counted outcome line, ✓ Deployed 3 rules to 2 tenants · 1 created, 2 updated · 12.4s. Those counts are the plan: they are predicted from .opentide/states/deployment/ and each rule's status before anything is sent, not reported back by the vendor. --json returns the same plan under changes, its tally under summary, and the selection:
opentide --json deploy --platform sentinel --dry-run --strategy PREVIEW --all{
"deployed": [],
"dry_run": true,
"plan": { "sentinel": ["00000000-0000-4000-8003-000000000001"] },
"payloads": {
"sentinel": [
{
"uuid": "00000000-0000-4000-8003-000000000001",
"name": "Sentinel KQL Rule",
"api_request": "..."
}
]
},
"missing_tenants": {
"sentinel": ".opentide/configurations/platforms/sentinel.toml"
},
"advice": "add (or uncomment) a [[tenants]] entry in .opentide/configurations/platforms/sentinel.toml",
"ok": true,
"status": "completed",
"message": "Plan: 0 to create, 0 to update, 1 to create or update, 0 to disable, 0 to delete",
"warnings": [
"sentinel has no tenants configured in .opentide/configurations/platforms/sentinel.toml, so a real deploy would stop"
]
}plan lists the rule UUIDs selected for each platform, and strategies the run's strategies. On a dry-run, tenants maps each platform to the tenants a real deploy would reach and the rules each would receive. payloads is present on dry-runs only; each rule's api_request (elided here) is the request body the deployer would send: the ARM alert-rule body for Sentinel, the Kibana rule body for Elastic, and the rule's platform configuration block for the other platforms. A Sentinel rule that would fail to deploy (an unparseable duration, a missing grouping or grouping.alert, or an empty response block with no alert.severity) is listed with "status": "failed", a code, an error naming the cause, a help, and a null api_request. diagnostics holds one entry per such rule (code, severity, message, help, a workspace-relative path, platform, uuid, name), and the dry-run reports failed and exits 1. The plan prints each one as a code frame, like validate and lint, above its tally, which keeps no glyph:
error[compile_empty_block]: 'response' is empty, so Sentinel cannot read 'alert_severity'
╭─▶ objects/rules/sentinel-kql-rule.yaml
╰─ help: set response.alert_severity, or configurations.sentinel.alert.severity
Plan: 0 to create, 0 to update, 0 to disable, 0 to delete, 1 would failThe rule's row in the plan reads ✗ would fail instead of its action, and the tally counts it apart (N would fail, summary.fail; each such entry of changes has "fails": true), since a rule that cannot compile is created or updated nowhere.
The codes are compile_missing_field, compile_empty_block, compile_invalid_value, compile_invalid_rule and compile_failed. A dry run sends nothing, so an unset $VARIABLE in a tenant's setup is a warning there (a real deploy would need $AZURE_CLIENT_SECRET, which is unset), not an error. The tutorial scaffold leaves [[tenants]] commented out, so this dry-run previews the payload, leaves deployed empty, and warns that a real deploy would stop. After you uncomment a tenant, deployed lists the platform and the warning is gone. A real deploy returns the same envelope with "dry_run": false and no payloads. Non-zero exit codes signal deployment errors.
When no rule the run read has a status mapping to its strategies, the command reports skipped with exit 0. The reserved deploy metadata command is hidden and returns a non-zero “not implemented” result rather than reporting false success.
Subcommands
deploy migrate-state
Moves vendor rule ids off rule files into deployment state. Run it once, in every workspace that has deployed to Microsoft Defender for Endpoint, SentinelOne or CrowdStrike with an earlier release. It calls no vendor API and deploys nothing.
opentide deploy migrate-state
opentide --json deploy migrate-stateWhat it does:
- Refuses a gitignored destination. If git ignores
.opentide/states/deployment/(the.opentide/states/line older setups wrote does), it exits1withdeployment_state_ignored, names the ignore line, and edits nothing. Replace that line with.opentide/states/sharing.jsonl; the command does not edit.gitignoreitself. - Scans every
.yamland.ymlfile underobjects/rules/, subfolders included, forrule_id,rule_id_bundleandrule_id::<tenant>at the root of each platform block. - Writes the Defender, SentinelOne and CrowdStrike ids to
.opentide/states/deployment/<uuid>.jsonfirst, flushed to disk, and only then removes the keys from the YAML. Comments, key order and whitespace are kept: only the id lines go. - Drops the keys of every other platform. Sentinel, Splunk, HarfangLab, Carbon Black Cloud and the rest address their rules without a vendor id, so their keys are removed and printed as
external_id_unused(platform, tenant, value, file), not stored. - Leaves a rule untouched and exits
1when a tenant is mapped twice or to a different id than the shard already holds (external_id_conflict), or when a value is not a usable id (external_id_shape: not a string or integer, an empty tenant or id, or a Defender or SentinelOne id that is not a plain decimal like12345). Other rules still migrate.
It exits 0 after a migration, after a second run with nothing left to move, and when the only findings are external_id_unused warnings. It exits 1 when any rule fails or git ignores the deployment state directory. If the command stopped after writing a shard and before editing the rule, the next run finds the two agree and finishes the edit.
The migration is done when it is committed. Commit the edited rules together with every new file under .opentide/states/deployment/. A deploy from a checkout that has the stripped rules but not the shards creates a second live rule for every one of them.
deploy metadata
Reserved stub. Hidden from --help; it deploys nothing and exits 2 with “not implemented”:
opentide deploy metadata --platform splunk
opentide --json deploy metadata --platform splunkerror: Metadata deployment is not implemented for splunk{
"error": "Metadata deployment is not implemented for splunk",
"ok": false,
"status": "failed",
"message": "Metadata deployment is not implemented for splunk"
}deploy metadata is not implemented. Do not rely on it to push lookup tables.
Troubleshooting
- Authentication error — credentials missing/wrong; see Troubleshooting.
AttributeError: module 'azure.mgmt.securityinsight.models' has no attribute 'AlertPropertyMapping'— through 0.14.2 thesentinelextra resolvedazure-mgmt-securityinsight1.0.0, which lacks that model, so every rule withalert.dynamic_propertiesfailed to deploy while the dry-run still passed. Since 0.16.0 opentide calls the ARM REST API directly and no longer uses that SDK; upgrade withpip install -U opentide.configurations.sentinel.grouping.alert is missing— Sentinel deploy needsgrouping.eventandgrouping.alert. The error names the rule UUID, anddeploy --dry-runreports it too. Through 0.14.2 it wasAttributeError: 'NoneType' object has no attribute 'enabled'.- A rule did not deploy — check the strategy its status maps to, the run's
--strategy, and the tenant'sstrategies. APREVIEWrun skips a rule inPRODUCTION(it maps toRELEASE), aRELEASErun skips a rule inDEVELOPMENT(it maps toPREVIEW, unless promotion rewrites it first),INERTrules never deploy, and a tenant withstrategies = ["RELEASE"]never receives aPREVIEWrun.deploycounts the rules a status kept out and names the statuses that would deploy (skippedin JSON); with--debug, therule_skipped_for_strategyandskipping_tenantlog lines name all three. Outside CI, deploy reads the last commit only: commit the rule, or pass--all. See Statuses, strategies and tenants. --strategy 'STAGING' is not a deployable strategy—STAGINGis a default status name (and was a deployment plan). Pass--strategy PREVIEW. The message names the strategy any configured status maps to.the tenant key 'deployment' was removed in 0.17.0— replace the tenant'sdeploymentline as the message says:STAGINGbecomesstrategies = ["PREVIEW"],PRODUCTIONbecomesstrategies = ["RELEASE"],ALWAYSis dropped,MANUALbecomesmanual = true.--plan was removed/DEPLOYMENT_PLAN was removed— use--strategy, and unsetDEPLOYMENT_PLANin your CI variables. Regenerate the pipeline withopentide setup ci <github|gitlab|azure>.deploy reads the rule files the last commit, push or pull request changed, which needs a git checkout— run inside the repository, or pass--all.warning: … rule file(s) in subfolders of objects/rules are not deployed— a rule file has to sit directly inobjects/rules/. A file in a subdirectory is outside the rule catalogue and is not deployed. Move it up intoobjects/rules/to deploy it. In CI, the warning names only the subdirectory files the change touched.- Platform not found, or
system_disabled_for_deployin the log — the platform is not enabled in your workspace config. A disabled platform is never selected and its engine never loads, for every--strategyand with--wide, even when the rule's own block saysenabled: true. The log names the command that enables it, such asopentide setup platforms --defender-for-endpoint.opentide validatereports such a block as aplatform_not_enabledwarning. external_id_in_rule— the rule still carriesrule_id,rule_id_bundleorrule_id::<tenant>on some platform block. Deploy leaves it out for every platform,--dry-runincluded, before any HTTP call, and the run exits1; other rules still deploy. Rundeploy migrate-stateand commit the result.external_id_missing(warning) — a deletion or disablement found no id for that tenant in.opentide/states/deployment/, so nothing is known to exist there and nothing is called. The run does not fail on it: retiring a rule that never reached a tenant is normal. If a remote copy does exist, remove or disable it by hand.deployment_state_unpersisted— a CI run other than--strategy RELEASEon a push (the pull-requestPREVIEWjob, for example) would create a Defender for Endpoint, SentinelOne or CrowdStrike rule, and nothing would commit the id it returns. The create was refused before any vendor call and the run exits1; updates and deletes of rules that already have an id still ran. The production job creates the rule after merge. Pass--persist-state-externallyonly if your pipeline commits.opentide/states/deployment/itself.deployment_state_ignored—.gitignorehides.opentide/states/deployment/, so deploy will not create a Defender, SentinelOne or CrowdStrike rule whose id git would never see. Ignore.opentide/states/sharing.jsonlinstead. See Deployment state for the otherdeployment_state_*codes.skippedwith no rules in a new repository — git does not keep empty folders, so a fresh clone has noobjects/rules/until the first rule is committed. Deploy treats the missing folder as an empty catalogue: it reportsskippedwith exit0, an emptyplan, and thedry_runflag, and contacts no platform.- No tenants —
setup repoenables a platform and leaves the[[tenants]]example commented out. A realdeployof rules for that platform exits1before any engine loads, and names the file to edit.--dry-runstill previews the payloads and warns that a real deploy would stop. - GitLab
No Source Commit Found— a run whose base SHA is not in the checkout, or a merge request pipeline with noCI_MERGE_REQUEST_DIFF_BASE_SHA, exits1with that sentence. Fetch full history (GIT_DEPTH: "0"), as the generated jobs do. Through 0.6.1 the same case crashed with'Repo' object has no attribute 'lookup_ref'. - GitLab push diff on a manual run, a schedule or a branch's first push — GitLab sets
CI_COMMIT_BEFORE_SHAto all zeros when there is no previous tip. The run then diffs from HEAD's first parent, as on GitHub and Azure. Through 0.16.1 it failed withNo Source Commit Found. - GitHub pull request diff without pull-request refs — the failure names
GITHUB_HEAD_REFandGITHUB_BASE_REF. Through 0.6.1 it asked for AzureSYSTEM_PULLREQUEST_SOURCEBRANCHandSYSTEM_PULLREQUEST_TARGETBRANCHNAME, thenKeyError. - Other CI diff failures — a missing tip SHA names
CI_COMMIT_SHA,GITHUB_SHA, orBUILD_SOURCEVERSION. An Azure pull request without its pull-request variables names those variables before fetching. A ref that is not onoriginnamesorigin/<name>. A one-parent GitLab merged result keeps the tip SHA. - GitHub production deploy skipped — through 0.6.3 the generated production job needed
deploy_staging, which runs only on a pull request, so a push to the default branch skipped production. 0.6.4 needsgenerateand still runs only on that push. Regenerate withopentide setup ci github --yes. DISABLEDorREMOVEDrule never retired — through 0.14.2--keep-deprecateddefaulted to off, so the default-branch deploy dropped those rules. ARELEASErun now keeps them by default; pass--drop-deprecatedfor the old behaviour. With--allthey are left out unless you pass--keep-deprecated.- Default-branch deploy empty or wrong after a merge commit — the GitHub and Azure push diff runs from HEAD's first parent (
HEAD^1), the default branch before the merge. Through 0.14.2 the base was the second commit by date, which on a merge commit is usually the merged branch's head: the rules the merge changed were not deployed, and a pull-request job on the synthetic merge ref reportedskipped. GitLab usesCI_COMMIT_BEFORE_SHAand was not affected. - Shallow checkout — deploy jobs fetch full history. A missing parent commit is named. Through 0.6.3 a depth-1 checkout exited
1with the parent SHA and no sentence.
Per-rule deployment via SDK or MCP
rule = Opentide.Rules[uuid]
result = rule.deploy("sentinel", dry_run=True) # as its status says
result = rule.deploy("sentinel", dry_run=True, strategies=["PREVIEW"])Without strategies, the rule is deployed by the run strategies that deploy the strategy its sentinel status maps to, to the tenants that accept them. MCP: deploy_rule(uuid, platform, dry_run=True) (defaults to dry-run) deploys the same way.
Source
src/opentide/cli/commands/deploy.py, src/opentide/cli/services/deploy.py, src/opentide/cli/services/migrate_state.py