The PingOne Advanced Identity Cloud pack (packs:ping-aic)
packs:ping-aic
The packs:ping-aic entitlement gates opschain-pack-ping-aic, the
signed commercial content pack for PingOne Advanced Identity Cloud
(formerly ForgeRock Identity Cloud): a tenant's configuration as YAML
resources your repository commits, reconciled by ping_aic:plan /
apply / export, and the tenant operations a governed change composes
— environment promotion, environment secrets and variables, identities,
service accounts, tenant logs. The pack is hand-typed Go over the
tenant's REST APIs via its own pingaic tool — no SDK, no vendor CLI, no
interpreter — executed by the runner, with outbound HTTPS from the runner
to the tenant as the only wire. The pack's signed manifest names
requires: packs:ping-aic, so the entitlement in your licence must spell
exactly that; like every packs:<name> grant it is validated by shape and
minted per customer (see entitlements).
The pack lives in the opschain-pack-ping family repository and mounts
like any other action pack — a packs: entry in the template's
.opschain/packs.yaml naming the pack remote, the ping-aic path and a
pinned revision (opschain docs getting-started/2-4-using-packs) — or
installs into the pack store as a signed bundle. A mount
name is an action namespace and may carry only lowercase letters, digits
and underscores, so the pack mounts as ping_aic and its actions read
ping_aic:plan, ping_aic:promotion_promote, and so on. Both doors
enforce the manifest: installing a signed bundle verifies its signature
and its required entitlement, and running any ping_aic:* action without
packs:ping-aic refuses with the named pack refusal before anything dials
the tenant.
The tenant and its identity
Every action targets a ping_tenant document — the tenant FQDN, its
environment tier (dev, staging or prod), the realms the declarative
resources cover, the environment variables the service account arrives
in, and whether the tenant opts into the gated api lane:
# .opschain/ping/tenant.yaml
kind: ping_tenant
name: acme-dev
spec:
tenant: openam-acme-dev.forgeblocks.com
tier: dev
realms: [alpha]
service_account:
id_env: PING_AIC_SA_ID
private_key_env: PING_AIC_SA_PRIVATE_KEY
api_lane: disabled
Commit one document per tenant environment and point each OpsChain
environment at its own through the ping_aic.tenant_file property.
Tenant environments then map onto OpsChain environments: path-based
authorisation governs who may touch production, and an approval policy on
the environment governs the promotion into it — the pack never invents an
approval of its own.
The pack dials the tenant as a service account. Its id and private
key (JWK or PEM) reach the step as environment the platform resolved from
vault references — opschain.env properties on the OpsChain environment
(opschain docs getting-started/3-5-secrets-and-the-vault):
{"opschain": {"env": {
"PING_AIC_SA_ID": "secret-vault://ping/dev/sa_id",
"PING_AIC_SA_PRIVATE_KEY": "secret-vault://ping/dev/sa_private_key"
}}}
Per step the tool mints a JWT-bearer assertion, exchanges it for a
short-lived access token and keeps the token in memory only. Each dial
logs one value-free line naming which credential variables were present
(kind and length only); a variable still carrying an unresolved vault
reference refuses the dial by name. Tenant logs authenticate with the log
API key pair the same way (PING_AIC_LOG_API_KEY / PING_AIC_LOG_API_SECRET).
Declarative resources
A resource is one YAML document — kind, name, realm for
realm-scoped kinds, and spec, the tenant object's own JSON expressed in
YAML. Ten kinds, in the order apply writes them:
| Kind | Scope | Tenant object |
|---|---|---|
ping_aic_esv | tenant | an environment variable (type: variable) or secret (type: secret — the value only ever comes from value_env, a vault-resolved environment variable, and is never read back) |
ping_aic_script | realm | a script; script is plain source in YAML and base64 on the wire; _id pins the identity |
ping_aic_journey | realm | an authentication journey with every node it references — spec.tree plus spec.nodes — as one promotable document |
ping_aic_oauth2_client | realm | an OAuth 2.0 client |
ping_aic_service | realm | a realm service's top-level configuration |
ping_aic_managed_object | tenant | one object of the managed-object configuration |
ping_aic_connector | tenant | a connector (provisioner) configuration |
ping_aic_sync_mapping | tenant | one synchronisation mapping |
ping_aic_email_template | tenant | an e-mail template |
ping_aic_theme | realm | one hosted-pages theme |
The semantics are what make the resources safe to commit and re-apply:
- A document owns the top-level keys it declares.
plancompares those keys with the live object (volatile fields stripped, encrypted values reduced to a marker) and an update sends the live object overlaid with the document, so undeclared keys keep their tenant values and a partial document stays idempotent across plans. ${values.<key>}placeholders take per-environment values from thevaluesinput — a JSON object or@file:<path>, defaulting to the environment'sping_aic.values_fileproperty — so one resource set serves dev, staging and prod; a placeholder with no value fails by name.- Secrets never sit in YAML: an ESV secret names
value_env; exported secrets carry no value; connector credentials are best declared as ESV placeholders the tenant resolves. - Deletes are opt-in per kind (
prune: ping_aic_journey,ping_aic_script) and scoped to the realms the documents cover. - Export is deterministic —
<kind>/<realm>/<name>.yaml(_globalfor tenant-wide kinds), sorted keys — so a repeat export of an unchanged tenant is byte-identical and an export followed by a plan is a no-op: adopting an existing tenant is an export, a review and a commit.
The shipped actions
Every action takes tenant_file (defaulting to the ping_aic.tenant_file
property, else .opschain/ping/tenant.yaml) and publishes its outputs
with outputs: passthrough.
ping_aic:plan/ping_aic:apply/ping_aic:export— the declarative lane:dir(default.opschain/ping/resources),values,prune, and the rendered plan inplan_file(plusplan_json) as the change's evidence; outputscreates,updates,deletes,unchanged,has_changes, and after applycreated/updated/deleted. Export takeskindsandrealms.ping_aic:config_export/config_import/config_diff/adopt— the configuration tree: one JSON object per file, global and per-realm, in the layout ForgeRock's sample configuration-manager tooling pulls, as the pack's import and adoption format. A repository already holding a tenant's configuration that way moves under OpsChain governance without conversion;adoptconverts a tree into YAML resources and reports directories outside the layout inunmapped, never dropping them silently.config_diffis the drift probe:has_changestells a workflow whether the committed tree and the tenant differ, andplan_filecarries the diff.ping_aic:promotion_lock/promotion_status/promotion_promote/promotion_rollback/promotion_unlock— dialled on the source (lower) environment's tenant: dev promotes into staging, staging into prod, and the step log announces the tiers.promotion_promotetakesdescription,dry_run,unlock_after,wait(default on) andwait_timeout, and lands the tenant's promotion report inreport_file;promotion_lockpublishes thepromotion_idthatpromotion_unlockreleases;promotion_statusis the wait predicate.ping_aic:esv_set/esv_list/esv_delete/restart/restart_status— environment secrets and variables. A variable's value may be an input; a secret's value always comes fromvalue_env.esv_listpublishes names only.restartapplies ESV changes and, withwait, polls until the tenant reports ready;restart_statusis the wait predicate.ping_aic:identity_get/identity_search/identity_create/identity_update/identity_delete— managed identities (object_type, defaultalpha_user), bulk throughfile(a JSON array or newline-delimited JSON). Outputs are masked by default: e-mail addresses, names, phone numbers and addresses reduce to a shape that identifies nothing while identifiers stay for the next step; a bulk step attempts every record and fails when any failed, with every outcome inoutput_file.ping_aic:service_account_create/service_account_rotate/service_account_delete— the pack's own bootstrap. The key pair is generated on the runner: the public key set goes to the tenant and the private key is written tokey_file(mode 0600) and nowhere else — not a step output, not the log — for a following step to store in the vault.ping_aic:logs_pull— tenant log events for asourceand time window (begin,end,filter,transaction_id) to a newline-delimited JSONoutput_file, masked by default: the evidence for an incident change.ping_aic:api— the escape hatch, gated harder than the typed actions (below).
Every action pre-warms the pinned pingaic binary (PATH, then the
runner cache, then the pack's own bundled tools/ directory, then a
sha256-verified download — never an unpinned public URL). A pack
installed from its signed bundle carries the tool, so a fresh runner
needs no toolchain and no download configuration.
The gated escape hatch
ping_aic:api dials one tenant-relative REST endpoint (method, path
with {realm} substituted from realm, body, query, api_version).
It refuses, with exit code 78, unless two independent gates are open:
- the tenant document declares
api_lane: enabled— the configuration repository's owners opted the tenant in, under review; - the step environment carries
PING_AIC_API_LANE=enabled— anopschain.envproperty the estate's administrators set on the OpsChain environment, so path-based authorisation governs who may grant it and an approval policy can sit on the environment.
The pack entitlement applies on top, and the step log announces every dial through the lane. Prefer a typed action; a gap worth this lane is a gap worth reporting.
Worked examples
Each example is a complete declarative action file
(.opschain/actions/<name>.yaml) composing the pack's actions with
uses: + with: (opschain docs orchestration). Nothing secret appears
in any of them — credential custody is server-side — and tenant_file
is omitted wherever the environment's ping_aic.tenant_file property
should carry it.
Prove the resources are in sync (ping_aic:plan)
# .opschain/actions/check_drift.yaml
action: check_drift
description: Compare the committed resources with the tenant and keep the plan as evidence
steps:
- name: plan
uses: ping_aic:plan
with: { dir: .opschain/ping/resources, plan_file: drift.txt }
- name: report
run: echo "drift=${{ steps.plan.outputs.has_changes }} (${{ steps.plan.outputs.updates }} updates)"
Reconcile, then promote with the report as evidence
The approval that governs the promotion into production is the approval policy on the OpsChain environment this change runs under.
# .opschain/actions/release_identity_config.yaml
action: release_identity_config
description: Apply the resources, then promote the environment with a dry run first
steps:
- name: apply
uses: ping_aic:apply
with: { dir: .opschain/ping/resources, prune: "ping_aic_journey,ping_aic_script" }
- name: lock
uses: ping_aic:promotion_lock
- name: rehearse
uses: ping_aic:promotion_promote
with: { dry_run: "true", report_file: promotion-dry-run.json }
- name: promote
uses: ping_aic:promotion_promote
with: { description: "identity config release", report_file: promotion-report.json }
- name: unlock
uses: ping_aic:promotion_unlock
with: { promotion_id: '${{ steps.lock.outputs.promotion_id }}' }
Rotate a secret and apply it (ping_aic:esv_set, ping_aic:restart)
The secret's value rides an opschain.env property carrying a vault
reference; the action names the variable, never the value.
# .opschain/actions/rotate_api_key.yaml
action: rotate_api_key
description: Replace the upstream API key and restart so the tenant picks it up
steps:
- name: set
uses: ping_aic:esv_set
with: { name: esv-api-key, type: secret, value_env: PING_AIC_ESV_API_KEY, description: "upstream API key" }
- name: restart
uses: ping_aic:restart
with: { wait_timeout: 20m }
Bulk identities, masked (ping_aic:identity_create)
# .opschain/actions/onboard_users.yaml
action: onboard_users
description: Create the quarter's new users from the committed file
steps:
- name: create
uses: ping_aic:identity_create
with: { object_type: alpha_user, file: onboarding/2026-q4.ndjson, output_file: onboarding-results.json }
- name: record
run: echo "created ${{ steps.create.outputs.succeeded }} of ${{ steps.create.outputs.count }}"
Adopt an existing configuration tree (ping_aic:adopt)
# .opschain/actions/adopt_config.yaml
action: adopt_config
description: Convert the tenant configuration tree the team already commits into YAML resources
steps:
- name: adopt
uses: ping_aic:adopt
with: { from: config/aic-dev, to: .opschain/ping/resources }
- name: record
run: echo "adopted ${{ steps.adopt.outputs.documents }} documents; unmapped: ${{ steps.adopt.outputs.unmapped }}"
Exit codes
64 usage · 65 validation · 68 not found · 73 conflict (a lock
already held, a revision mismatch) · 75 throttled (after
Retry-After-honouring retries) · 77 authorization · 78 gated · 1
anything else. Workflows branch on the class, not on message scraping.
Entitlement and lapse behaviour
The signed-pack gate, uniformly: a LimePoint-signed pack whose verified
manifest requires packs:ping-aic is gated on that feature at install and
at every action run — the refusal names the pack, the mount and the
missing entitlement, and lands a customer-visible licence warning event.
The grant is layered by a pack-tier licence document beside your base
licence and stops contributing once that document expires past its grace
window: installed content and stored configuration are never removed,
reads stay up, and everything lights back up when a renewed licence is
applied.
Where the rest lives
- Entitlements — the pack vocabulary and refusal shapes.
- The pack store — installing signed bundles.
- Secrets providers — the vault references the service account and ESV values ride.
opschain docs getting-started/2-4-using-packs— mounting a pack onto a template.