Skip to content

docs(deploy): two-phase deploy, revert, by-reference deploys, and OIDC trusted publishing - #599

Draft
dawsontoth wants to merge 31 commits into
mainfrom
claude/two-phase-deploy-docs
Draft

docs(deploy): two-phase deploy, revert, by-reference deploys, and OIDC trusted publishing#599
dawsontoth wants to merge 31 commits into
mainfrom
claude/two-phase-deploy-docs

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Draft, and its upstream is closed rather than pending. This documents the two-phase deploy_component work from harper#1849, which was closed without merging — along with #2135 and #2301, both of which were stacked on its branch rather than on main. None of revert_component, two_phase, activateStagedApplication, deployComponentTwoPhase or stagingRetention exists in main, and harper is at v5.2.5.

So this is not waiting on a merge. It is waiting on a decision about whether the feature is being revived. Someone who owns that call should make it; the docs should follow rather than sit here. If the work returns in a different shape, this drafting is a starting point but should be re-verified against whatever lands, not trusted.

Everything whose upstream is settled has been extracted so it stopped waiting on this:

Content Now in Why it could leave
by-reference deploys, sealed credentials, SSH keygen #636 (merged) shipped in v5.2.3 / v5.2.4
CLI token credentials #630 (merged) shipped in v5.2.0
OIDC trusted publishing, ops-table anchors, badge guidance #637 harper#2173 merged; anchors have no dependency

Held here deliberately, despite looking extractable: the deployment status groupings and the retention text. list_deployments and get_deployment are shipped, so those corrections read as generic tracking fixes — but they enumerate staged, staging, activating, reverting, extracting and loading, while main defines only pending, installing, replicating, restarting, success, failed and rolled_back. Extracting them would document statuses that do not exist. The retention text likewise mixes the shipped payload bound with the unshipped staging one.

Fourteen findings from this PR's last review round remain unverified, including a major on the documented payload-retention bounds not matching pruning behavior. They were recorded rather than checked. Given that a third of the automated findings on the sibling PRs turned out to be false positives from reading the wrong branch, each needs verifying against whatever the two-phase implementation finally looks like — which is another reason to settle the feature's status first.

What this documents

Six files, not the two the previous description listed. The cluster-wide deploy lifecycle, rollback, deploy-by-reference, sealed deploy credentials, server-side SSH keygen, and OIDC trusted publishing.

Surface Upstream State
Surface Upstream Ships in
--- --- ---
deploy by_ref=true / ref= / credential= harper#1850 v5.2.3 — shipped
deploy setup=true harper#1851 v5.2.3 — shipped
add_ssh_key generate: true harper-pro#594 v5.2.4 — shipped
OIDC trusted publishing harper#2173 merged, unreleased → v5.3.0
Two-phase deploy, revert_component, staging retention harper#1849 still a draft → v5.3.0

Two things changed since the last review pass

The versions have moved twice, and are no longer uniform. harper main is at 5.2.4, and git tag --contains on each merge commit puts by-reference deploys and sealed credentials in v5.2.3 and server-side SSH keygen in v5.2.4 — all three are in users' hands already. OIDC merged 2026-08-21 and is unreleased; two-phase is still a draft. Both of those carry v5.3.0, confirmed by the 5-3-0.ts upgrade directive on main. Release notes are split the same way: the shipped features moved to 5.2.md labelled with their patch, and 5.3.md keeps two-phase and OIDC.

The one piece that genuinely shipped in v5.2.0 has left. login --for-ci and the token env vars (harper#1876, in v5.2.0 since 2026-07-31) are now #630, against main, mergeable today. That answers @Ethan-Arrowood's first sequencing question — the shipped half no longer waits on four unmerged PRs — and it puts the token rules inside main's canonical numbered Authentication Precedence section rather than in a competing paragraph beside it.

Review feedback

@kriszyp's eight findings (CHANGES_REQUESTED), each re-checked against upstream as it stands now rather than as it stood when filed. Four had been fixed at the source in the meantime.

# Finding Resolution
1 "All-or-nothing at go-live" is wrong Docs fixed. Reframed as an all-nodes staging barrier.
2 Deploy modes presented as independent knobs Fixed upstream, now documented as a matrix.
3 Explicit ref not resolved to a SHA Fixed upstream (5c85714b8, 26cce986d, 21043f994).
4 Rollback scope and restart claims too broad Docs fixed. Scoped to the two-phase path; restart documented as optional.
5 Absolute SSH-key encryption guarantee Docs fixed. Qualified — the plaintext fallback is deliberate upstream.
6 auth_* vs. ordinary username= precedence Fixed upstream; main's canonical list covers it, #630 extends it.
7 Plain "Available since" instead of badges; release notes Docs fixed. Badges + new 5.3.md.
8 Missing badges, summary-table row, release notes Docs fixed. Plus the revert_component row.

Worth expanding on three:

  • Finding 1 was a self-contradiction, not just imprecision: the page claimed all-or-nothing in its opening paragraph and then correctly described a version split twenty lines later. The staging barrier is what the two phases buy — it eliminates the common failure, a node that can't fetch or install — but activation is still per node. The activation-failure discussion is now its own #### Activation failures section rather than sitting mid-parameter-list, and the opening paragraph points at it.
  • Finding 2 became documentable because core added the validation @kriszyp asked for. activate: false and deployment_id are rejected when combined with two_phase: false, replicated: false, or an unreplicated system database, so a request that says stage-only can no longer activate. There is now a valid-combinations table, including that revert_on_failure is refused rather than ignored.
  • Finding 5 stands as a docs problem because core chose to keep the fallback: sealSSHKey still stores and replicates the private key in plaintext with a WARN when no secret custody is registered, deliberately, since SSH keys predate custody. So encryption at rest is a property of the deployment's configuration, not of the operation — stated as a warning, while noting custody is present by default so the risk doesn't read as likely.

@Ethan-Arrowood's structural hold is addressed:

  1. Sequencing — split, as above. The remaining content is uniformly unshipped v5.3.0, and the machine-readable marker at the top of this description arms it for #629's companion check.
  2. The conflict with main — resolved by hand, and it was the hazard he predicted. The ops table needed a three-way merge: main's seven backup rows (create_backup, list_backups, verify_backup, delete_backup, purge_backups, restore_backup, get_backup), this branch's four deploy rows, and this branch's recategorization. Taking either side wholesale would have silently dropped one set. I compared both sides row by row first and confirmed no description text differed, so the width churn is only Prettier re-padding. authentication.md no longer states precedence twice, since docs(cli): token credentials in the canonical auth precedence (v5.2.0) #630 owns that file's content.
  3. Duplicate precedence, stale body, missing config keys, (draft) in the title — the first is gone with the split, this description is rewritten, the title marker is dropped in favour of GitHub's own draft state.
  4. The system-replication precondition (his second question) — now documented. A plain deploy on a cluster with system excluded from replication silently takes the one-shot path, which also means no rollback target; the staged parameters are rejected rather than downgraded.

@cb1kenobi's five findings were fixed in the previous pass and remain so.

Still open

  • deployment_stagingRetention_maxCount and deployment_payloadRetention_* are documented inline, not in reference/configuration/options.md. @Ethan-Arrowood is right that operators look there, and right that the original justification ("matching how payloadRetention_maxSize is surfaced") was false — it isn't surfaced anywhere. Left as-is deliberately: three config keys for an unreleased feature seem better added when the options page can be updated in one pass against the shipping release. Happy to add them here instead.
  • Most of this is no longer gated. Only two-phase and revert_component depend on an unmerged PR, so the marker names harper#1849 alone. Everything else is shipped or merged — which makes splitting the ungated content into its own PR worth considering rather than holding it behind a draft.

Verification

npm run build and npm run format:check clean. The build reports two broken anchors; both are pre-existing on main (confirmed by building main with these changes stashed) and neither is in a file this PR touches. Claims re-verified against the upstream branches; harper#2173's description has since been updated by its author and no longer diverges. Fourteen findings from the third cross-model round remain unaddressed and unverified — see the review-coverage note below.

Description drafted by Claude Code (Opus 5).

…ivate CLI

Document the two-phase deploy behavior and the new surface from HarperFast/harper#1849:

- deploy_component: cluster-wide stage -> barrier -> activate; new properties
  activate:false (stage-and-stop, returns a staged deployment_id), deployment_id
  (activate a previously-staged deployment), revert_on_failure, two_phase, and
  the ignore_replication_errors / deployment_timeout knobs.
- revert_component: fast cluster-wide rollback to the retained previous version.
- Deployment lifecycle: stage/activate phase names, staged/staging/activating/
  rolled_back statuses, and staged-build retention (deployment_stagingRetention_maxCount).
- CLI: harper stage / activate / revert verbs (aliases + examples).

Companion to HarperFast/harper#1849 (still a draft).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the documentation for the CLI and Operations API to reflect the new two-phase deployment process (staging and activation) and the rollback mechanism (revert_component). It introduces new CLI commands, aliases, API parameters, and examples. The review feedback suggests improving consistency by pointing the new CLI operation category links to the main #components section, and enhancing readability in the API documentation by adding clear labels to the multi-step "Stage now, activate later" JSON examples.

Comment thread reference/cli/operations-api-commands.md Outdated
Comment thread reference/operations-api/operations.md
@github-actions
github-actions Bot temporarily deployed to pr-599 July 21, 2026 14:32 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

…tage/activate example

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to pr-599 July 21, 2026 14:42 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

@dawsontoth
dawsontoth marked this pull request as ready for review July 29, 2026 17:53
@dawsontoth
dawsontoth requested a review from a team as a code owner July 29, 2026 17:53
Conflict in reference/operations-api/operations.md was additive: main added the
`credentials` parameter plus its "Deploy credentials" section (#581) where this
branch adds the two-phase parameters. Kept both — the parameter list now carries
activate/deployment_id/revert_on_failure/ignore_replication_errors/
deployment_timeout/two_phase AND credentials, followed by main's credentials
section. main's get_deployment_payload / delete_deployment_payload docs (#600)
are untouched.
… values

Documents harper-pro#594 and syncs with harper#1849's latest behavior.

add_ssh_key `generate: true` (harper-pro#594):
- Harper mints the ed25519 keypair on the node and returns only `public_key`,
  so the private key is never carried in a request body, shell history, or CI
  log. Includes the response shape and the `harper:<name>` comment.
- `key` and `generate` are mutually exclusive; ssh-keygen must be on PATH.
- Notes that `public_key` is returned ONLY on the generating call — Harper does
  not retain it, and update_ssh_key requires a key you supply (it cannot mint
  one), so the recovery path is delete_ssh_key + add_ssh_key generate again.
- CLI example added alongside the deploy/stage/activate/revert examples.

Payload retention (harper#1849):
- Splits the old one-line aside into its own subsection with a table covering
  both bounds: payloadRetention_maxSize (10 MiB, bounds a single payload) and
  the new payloadRetention_maxCount (default 1, bounds how many are kept per
  project). Explains why the default is conservative, that rows are never
  deleted, and that a reclaimed payload reports payload_blob_present: false.

Also:
- revert_on_failure / ignore_replication_errors now note they apply to a
  `deployment_id` activate too, matching the peer-failure gate added to that
  path.
- list_deployments `status` listed only 5 of 13 real values; replaced with
  terminal vs in-flight groupings.
- New note on restartRequired: deploying a brand-new component without a
  restart marks one as required (per node), while redeploying a live component
  does not.
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

@github-actions
github-actions Bot temporarily deployed to pr-599 July 29, 2026 18:03 Inactive
…cument them

The SSH rows linked to #certificate-management, but the SSH operations are
documented under ## Components — so every one of those seven links landed the
reader in the wrong section. Repointed them to #components (the convention the
other 15 component rows already use).

Auditing the same block turned up the identical defect on the six user/role rows
(list_users, add_user, alter_user, drop_user, list_roles, drop_role), which are
documented under ## Users & Roles. Repointed those to #users--roles, matching the
existing link to that section elsewhere in the docs.

The five genuine certificate rows (create_csr, sign_certificate,
list_certificates, add_certificate, remove_certificate) were already correct and
are unchanged. Category labels updated to match their new targets.

Verified every operations.md anchor referenced from this table against the real
headings — all 11 resolve.
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

Builds on the two-phase deploy docs (#599) to cover the rest of the
deploy-by-reference workflow.

reference/components/applications.md
- "Deploying by Reference" — `harper deploy by_ref=true`, `ref=`, `credential=`.
  Explains why a reference pins to a resolved SHA rather than the tag or branch
  name typed: peers resolve the package independently, so a tag that moves
  mid-deploy could otherwise leave nodes running different code. Notes that the
  cluster clones the pushed remote, and that by-reference means the cluster
  builds from source — so an app whose build can't run on the node should stay
  on payload deploys.
- "Provisioning a Deploy Credential" — `harper deploy setup=true`, the
  client-side sealing flow, and that reverting to the previous version needs no
  credential at all.

reference/cli/authentication.md
- Documents HARPER_CLI_REFRESH_TOKEN / HARPER_CLI_OPERATION_TOKEN and adds
  "Token credentials for CI/CD", so the CI guidance is no longer "put an admin
  password in your pipeline". Covers precedence, in-memory-only refresh, and
  token lifetimes (1d / 30d defaults).
- Warns that a user holds only ONE valid refresh token at a time: issuing a new
  one invalidates the last, so a routine local `harper login` silently breaks a
  pipeline using the same account. Hence the recommendation to give CI its own
  user. (Verified against security/tokenAuthentication.ts, which stores a single
  hashed refresh_token per user record.)

reference/cli/commands.md
- `harper login --for-ci`, including the stdout/stderr split that makes
  `| gh secret set --env-file -` work without displaying the token.

reference/security/secrets.md
- Points the existing private-source deploy credentials section at the CLI flow
  that automates it.

Documents HarperFast/harper#1850, #1851, and #1876. Verified every anchor link
in the changed files resolves (16 checked).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Opened #616 stacked on this branch: docs(deploy): by-reference deploys, sealed credentials, CI token auth.

It targets claude/two-phase-deploy-docs rather than main, so its diff shows only the new material (+139, no deletions, no overlap with this PR). Merge this one first and #616 retargets to main automatically.

Where the two meet: this PR documents the two-phase deploy and revert_component; #616 documents how you point a cluster at a specific commit in the first place (harper deploy by_ref=true), how it authenticates to a private source (harper deploy setup=true), and how CI authenticates to the cluster (harper login --for-ci + HARPER_CLI_REFRESH_TOKEN). It leans on your revert_component section for the point that a rollback to the previous version needs no credential at all.

Comment thread reference/operations-api/operations.md Outdated
Comment thread reference/operations-api/operations.md Outdated
Comment thread reference/operations-api/operations.md Outdated
Comment thread reference/operations-api/operations.md Outdated
Comment thread reference/operations-api/operations.md Outdated
Comment thread reference/cli/operations-api-commands.md Outdated
…vert caveat

Addresses cb1kenobi's review on #599.

- "Terminal" wrongly included `staged`. The implementation's TERMINAL_STATUSES is
  {success, failed, rolled_back} and that set gates get_deployment_payload and
  payload-pruning eligibility, so listing `staged` as terminal contradicted the
  get_deployment_payload section. Split into Terminal / Resting / In flight, and
  noted why a resting payload is deliberately still held.
- get_deployment's `status` row listed 7 of 13 values; now lists all, pointing at
  the grouped explanation.
- The `restart: "rolling"` example's response showed the no-restart message.
  Corrected to the rolling shape (restartJobId + ", restarting Harper") and noted
  what the no-restart response looks like instead.
- revert_component: added a caution that reverting swaps live directories and does
  NOT rewrite the stored `package:` reference, so a node provisioned after a revert
  installs the reverted-away version. Verified in code: revertComponent never calls
  writeComponentRootConfig, and installApplications() installs from root config.
- deployment_id activate: documents that the staged package identifier and
  credential references are recovered and persisted at activation (fixed in
  harper#1849), so `package` need not be repeated on the activate call.
dawsontoth and others added 2 commits August 21, 2026 16:59
`HARPER_CLI_REFRESH_TOKEN` / `HARPER_CLI_OPERATION_TOKEN` shipped in v5.2.0
(harper#1876) but never reached the reference docs. The Authentication
Precedence list stopped at the saved `harper login` token, so the two variables
a CI pipeline is supposed to use were absent from the one place that states
which credential wins.

Folded into main's existing numbered list rather than stated separately, so the
order is given once: bearer tokens sit at position 5, above the saved login
token and above the legacy `username=`/`password=` fallback, which is what
cliOperations.ts does.

Also documents the parts that are only discoverable by reading the source:

- Token variables apply to remote targets only. A local operation goes over the
  domain socket and is already trusted, so attaching a bearer token there would
  opt out of that trust and 401 — hence the deliberate ignore.
- A token namespace owns both halves. `HARPER_CLI_OPERATION_TOKEN` cannot pair
  with `CLI_TARGET_REFRESH_TOKEN`, which would otherwise run as one identity
  until expiry and then silently continue as another.
- A set-but-blank namespace reports and falls back rather than silently running
  as whoever last logged in locally.
- An env-sourced refreshed token stays in memory; there is no file entry to
  persist it to.
- One refresh-token hash per user, so `--for-ci` as your own account revokes
  your own token. Hence the dedicated-CI-user warning.

`harper login --for-ci` is documented in commands.md with the stdout/stderr
split that makes `| gh secret set --env-file -` work without displaying the
token.

Verified against harper v5.2.2: bin/cliOperations.ts (precedence and refresh),
bin/login.ts (--for-ci output), config/configUtils.ts (1d / 30d defaults).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The other half of @kriszyp's discoverability finding, for the piece that
actually shipped in v5.2.0. A user who only reads release notes had no way to
discover that a pipeline can authenticate with a token instead of an admin
password.

States the two properties that change how someone sets this up: the token
variables outrank a saved login (so a runner with both uses the CI identity),
and one refresh-token hash per user means issuing a token for CI revokes your
own if you run it as yourself.

The 5.3 surface goes in 5.3.md, added on #599.
@dawsontoth
dawsontoth force-pushed the claude/cli-token-auth-docs branch from 888a5fd to 50292cb Compare August 21, 2026 21:00
#630 was rebased onto main (12 new commits, mostly analytics docs), so this
branch picks up the rewritten base.

Three conflicts in `authentication.md`, all resolved to this branch's side — the
precedence list with OIDC at 7, the corrected blank-token paragraph, and the
workload-identity section #630 does not have. Resolved hunk by hunk rather than
with `checkout --ours`, which is what silently dropped content the last time.

One of those conflicts is worth noting: #630 still carries the original
"an error, not a fallback" wording for a blank token namespace, because that
paragraph is #630's content and the correction was only ever applied here. Being
backported to #630 separately — otherwise #630 publishes the self-contradictory
version if it merges first, which it is meant to.
dawsontoth added a commit that referenced this pull request Aug 21, 2026
The paragraph contradicted itself: "an error, not a fallback", then described the
fallback, then denied the behavior it had just described. The CLI warns and
continues down the precedence list, which does mean running as whoever last
logged in on that machine — the warning is the only thing that makes it
non-silent.

Caught by a review pass against #599, where this text also lives. Fixing it only
there would have left this PR publishing the wrong version, since this is the PR
that owns the paragraph and the one meant to merge first.
Re-checked against harper's tags, which have moved twice since the badges were
set. main is now 5.2.4, and `git tag --contains` on each merge commit says:

| Surface | Upstream | Ships in |
| --- | --- | --- |
| `deploy by_ref=true` | harper#1850 | **v5.2.3** |
| `deploy setup=true` | harper#1851 | **v5.2.3** |
| `add_ssh_key generate` | harper-pro#594 | **v5.2.4** |
| OIDC trusted publishing | harper#2173 | merged, unreleased -> v5.3.0 |
| two-phase, `revert_component` | harper#1849 | still draft -> v5.3.0 |

So the badges were wrong in the other direction this time: content marked v5.3.0
has been in users' hands since 5.2.3. Corrected the four affected markers —
Deploying by Reference, Provisioning a Deploy Credential, the `deploy setup`
mention in secrets.md, and the `generate` subsection. The two-phase and OIDC
badges stay at v5.3.0.

Release notes follow the same split. `5.3.md` keeps two-phase, stage/activate,
rollback, and OIDC; by-reference deploys, sealed credentials, and server-side SSH
keygen move to `5.2.md` under Components and Security, each labelled with the
patch that shipped it. The repo keeps one page per minor and points patches at
GitHub releases, so a feature that landed in a patch is otherwise undiscoverable
from the docs.

That the badges were wrong twice, in opposite directions, is the argument for the
`git tag --contains` check now recorded in AGENTS.md rather than reading a
branch's package.json.
@github-actions
github-actions Bot temporarily deployed to pr-599 August 21, 2026 21:06 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

dawsontoth added a commit that referenced this pull request Aug 24, 2026
Per review direction: guide readers to pick a single credential style rather than
leaving them to combine a token with raw credentials.

Checking the existing text rather than assuming it already said this — it did
not, quite. Line 111 said "prefer a token over `HARPER_CLI_PASSWORD`", which is a
preference between two *environment* credentials and silent on the payload
`username=`/`password=` pair. The part that matters, keeping payload credentials
off a command a token is meant to authenticate, sat inside the 403 warning, so it
only reached a reader who opened that box.

Now a single rule directly under the precedence list, where it belongs: precedence
exists to resolve a conflict and resolves it silently, and every bad outcome this
page documents needs two styles live at once — a payload pair taking over when a
token stops resolving, a blank token variable handing the run to a saved login.
A small table maps context to style (pipeline / one-off admin command / local
development). The 403 warning and the pipeline advice now defer to the rule
instead of each carrying a partial version of it.

Also drops a `#workload-identity-oidc` link I had reached for out of habit: that
section lives on #599, not this branch, and the build caught it as a broken
anchor. Back to the two pre-existing anchors on main.
dawsontoth added a commit that referenced this pull request Aug 25, 2026
The other half of @kriszyp's discoverability finding, for the piece that
actually shipped in v5.2.0. A user who only reads release notes had no way to
discover that a pipeline can authenticate with a token instead of an admin
password.

States the two properties that change how someone sets this up: the token
variables outrank a saved login (so a runner with both uses the CI identity),
and one refresh-token hash per user means issuing a token for CI revokes your
own if you run it as yourself.

The 5.3 surface goes in 5.3.md, added on #599.
dawsontoth added a commit that referenced this pull request Aug 25, 2026
The paragraph contradicted itself: "an error, not a fallback", then described the
fallback, then denied the behavior it had just described. The CLI warns and
continues down the precedence list, which does mean running as whoever last
logged in on that machine — the warning is the only thing that makes it
non-silent.

Caught by a review pass against #599, where this text also lives. Fixing it only
there would have left this PR publishing the wrong version, since this is the PR
that owns the paragraph and the one meant to merge first.
dawsontoth added a commit that referenced this pull request Aug 25, 2026
Per review direction: guide readers to pick a single credential style rather than
leaving them to combine a token with raw credentials.

Checking the existing text rather than assuming it already said this — it did
not, quite. Line 111 said "prefer a token over `HARPER_CLI_PASSWORD`", which is a
preference between two *environment* credentials and silent on the payload
`username=`/`password=` pair. The part that matters, keeping payload credentials
off a command a token is meant to authenticate, sat inside the 403 warning, so it
only reached a reader who opened that box.

Now a single rule directly under the precedence list, where it belongs: precedence
exists to resolve a conflict and resolves it silently, and every bad outcome this
page documents needs two styles live at once — a payload pair taking over when a
token stops resolving, a blank token variable handing the run to a saved login.
A small table maps context to style (pipeline / one-off admin command / local
development). The 403 warning and the pipeline advice now defer to the rule
instead of each carrying a partial version of it.

Also drops a `#workload-identity-oidc` link I had reached for out of habit: that
section lives on #599, not this branch, and the build caught it as a broken
anchor. Back to the two pre-existing anchors on main.
Base automatically changed from claude/cli-token-auth-docs to main August 25, 2026 14:29
#630 merged, so this branch retargeted to `main` and conflicted on the three
files it shares. The conflict was lopsided rather than symmetric: this branch
carried the token content as it stood when #630 was first merged in, while `main`
now has that same content after fourteen review rounds — the one-style rule, the
403-on-expiry warning, the loopback/`authorizeLocal` material, the corrected
saved-target fallback, `CLI_TARGET_OPERATION_TOKEN`, and the trimmed rationale.

So resolving hunk-by-hunk would have been reconciling an obsolete copy against a
corrected one. Instead: took `main`'s version of all three files wholesale,
verified each was byte-identical to `main`, then re-applied this branch's only
real contribution to them — the OIDC additions — on top. `authentication.md` is
now +53 against `main` rather than a full-file conflict.

Two things the re-application had to reconcile, because `main` gained content that
interacts with OIDC:

- The rejected-exchange paragraph said the operation "returns 401" when nothing
  else is configured. That is now only true off-loopback: per the hinge @kriszyp
  established, a credential-less request to a loopback node is authorized as
  superuser, so a policy mismatch there succeeds with no identity check. Stated,
  and cross-linked to the refresh-behavior note rather than repeating it.
- Workload identity added to the one-style table's CI/CD row, since it is now a
  second valid choice for that context.

Also caught myself re-introducing an inline `<VersionBadge>` in the precedence
list — the same CONTRIBUTING violation I fixed on #630 an hour ago. Plain text.
@github-actions
github-actions Bot temporarily deployed to pr-599 August 25, 2026 14:35 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

dawsontoth added a commit that referenced this pull request Aug 25, 2026
Same reasoning as the by_ref content: harper-pro#594 merged 2026-08-20 and
`git tag --contains` puts it in **v5.2.4**, so it is shipped and should not wait
on harper#1849.

- `generate: true` mints an ed25519 keypair on the node and returns only the
  public half, so a deploy key's private half never travels in a request body,
  shell history, or CI log. Generation is in-process (`node:crypto`), needing no
  `ssh-keygen` binary on the host.
- `public_key` comes back only on the generating call — Harper does not retain
  it — so the recovery path is delete-and-regenerate, since `update_ssh_key`
  cannot mint.
- The encryption-at-rest claim is qualified rather than absolute. `sealSSHKey`
  falls back to storing and replicating the private key in **plaintext** with only
  a WARN when no secret custody is registered, and that fallback is deliberate —
  SSH keys predate custody. So it is a property of the deployment's
  configuration, not of the operation, which matters most for `generate: true`
  because that reads as though the key could never be exposed. Custody is present
  by default, and the note says so rather than implying the risk is likely.

Also restores the 5.2.4 release-note entry, which I dropped when I took main's
`5.2.md` wholesale during the #599 merge — the same whole-file resolution hazard
that cost content once already in this session. Verifying the file matched main
confirmed the take succeeded but said nothing about what #599 uniquely added.
Main gained four `node.hostname` commits while this branch was being restructured,
one of which created `release-notes/v5-lincoln/5.3.md` independently of the copy
this branch already had. That produced an add/add conflict.

Resolved by combining rather than choosing: this branch's Deployment and Security
sections (two-phase, stage/activate, rollback, OIDC) plus main's Node Identity
section. Picking either side would have silently dropped a release's worth of
notes for the other's feature.

Worth noting how this surfaced — not from the conflict, but from `options.md`
appearing in this branch's diff as though it were *reverting* main's new `## node`
section. A docs branch showing deletions it never made is the signal that its base
has moved.
Removes what #636 now owns, so this PR is purely surface gated on
harper#1849 (draft) or unreleased:

- `reference/components/applications.md` — Deploying by Reference and
  Provisioning a Deploy Credential (both v5.2.3). The page returns to Package
  Sources followed by Dependency Management.
- `reference/operations-api/operations.md` — the `add_ssh_key` section reverts to
  main's, handing the `generate` subsection and its custody caveat (v5.2.4) over.
- `reference/security/secrets.md` — the `deploy setup=true` pointer.

One point was relocated rather than dropped. The credential section carried a
note that rolling back needs no credential, which is a fact about
`revert_component` rather than about provisioning, and `revert_component` stays
here. It now sits in that operation's own docs, phrased for why it matters: a
revert re-fetches nothing, so it still works when the token or deploy key that
installed the current version has expired or been revoked — which is often the
situation you are in when you need to roll back.

What remains is two-phase deploy, `revert_component`, the deployment lifecycle
and retention, the CLI stage/activate/revert rows, and OIDC.
@github-actions
github-actions Bot temporarily deployed to pr-599 August 25, 2026 15:12 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

dawsontoth added a commit that referenced this pull request Aug 25, 2026
Splits the shipped half of #599 out so it can merge on its own. Everything here
documents behavior in **v5.2.3** — `git tag --contains` on harper#1850's and
harper#1851's merge commits returns v5.2.3 and v5.2.4 — while #599 is gated on
harper#1849, still a draft. Accurate docs for a shipped feature should not wait
on unshipped work.

Carries the corrected text, not #599's original: the corrections that lived in
#617 are folded in here, so there is no window where the published page describes
the superseded parameter form.

- **Deploying by Reference** — `harper deploy by_ref=true`, `ref=`,
  `credential=true`. A reference pins to a resolved SHA: refs resolve from the
  local checkout, then from the remote when it is absent (a shallow CI clone
  usually has neither), and the deploy fails closed if neither can name a commit,
  because peers resolve the package independently. A ref must also name something
  a clone can fetch — `refs/heads/*` or `refs/tags/*` — so `refs/pull/123/head`
  is rejected up front even where the local checkout resolves it. A pinned SHA
  fixes the source revision, not the built artifact.
- **`credential=true`** derives the host from the package, so the credential
  always matches the clone it authenticates; an explicitly named host that does
  not match is rejected rather than silently unused.
- **GitHub Actions behavior** — the unpushed-commit check is skipped there (the
  runner's checkout is not a branch `git branch -r --contains` can see) while the
  dirty-tree warning still applies, and a `pull_request` run deploys the PR's
  head commit rather than the merge commit the runner checks out.
- **Provisioning a Deploy Credential** — `harper deploy setup=true`, the
  client-side sealing flow, the two providers it supports, and that it needs
  super_user because it calls `get_secrets_public_key` and `set_secret`.
- `reference/security/secrets.md` points its private-source section at the CLI
  flow, and `5.2.md` gains the two release-note entries.

The `revert_component` cross-reference from the credential section is deliberately
left on #599: `revert_component` is unshipped, and the build caught the dangling
anchor when it came across.
dawsontoth added a commit that referenced this pull request Aug 25, 2026
Same reasoning as the by_ref content: harper-pro#594 merged 2026-08-20 and
`git tag --contains` puts it in **v5.2.4**, so it is shipped and should not wait
on harper#1849.

- `generate: true` mints an ed25519 keypair on the node and returns only the
  public half, so a deploy key's private half never travels in a request body,
  shell history, or CI log. Generation is in-process (`node:crypto`), needing no
  `ssh-keygen` binary on the host.
- `public_key` comes back only on the generating call — Harper does not retain
  it — so the recovery path is delete-and-regenerate, since `update_ssh_key`
  cannot mint.
- The encryption-at-rest claim is qualified rather than absolute. `sealSSHKey`
  falls back to storing and replicating the private key in **plaintext** with only
  a WARN when no secret custody is registered, and that fallback is deliberate —
  SSH keys predate custody. So it is a property of the deployment's
  configuration, not of the operation, which matters most for `generate: true`
  because that reads as though the key could never be exposed. Custody is present
  by default, and the note says so rather than implying the risk is likely.

Also restores the 5.2.4 release-note entry, which I dropped when I took main's
`5.2.md` wholesale during the #599 merge — the same whole-file resolution hazard
that cost content once already in this session. Verifying the file matched main
confirmed the take succeeded but said nothing about what #599 uniquely added.
harper#1849 is closed, and so are #2135 and #2301 which were stacked on it —
#2135's base was `claude/deploy-component-two-phase-94969a`, not `main`. None of
`revert_component`, `two_phase`, `activateStagedApplication`,
`deployComponentTwoPhase` or `stagingRetention` exists in `main`.

So this PR's subject is not merely unmerged, it is abandoned in that form. There
was no reason for the content whose upstream *is* settled to wait with it.
Extracted to #637:

- **OIDC trusted publishing** — harper#2173 is merged (2026-08-21), awaiting
  v5.3.0. The API is fixed, which is the difference that matters: the risk here
  was never "unreleased", it was "changing underneath the docs".
- **The ops-table anchor fix** — 13 rows pointing at `#certificate-management`,
  which documents neither user/role nor SSH operations. A pre-existing docs bug
  with no upstream dependency, which should never have been behind a feature
  branch.
- **The `AGENTS.md` badge guidance.**

What stays is two-phase deploy, `revert_component`, deploy modes, activation
failures, the deployment status groupings, retention, and the 5.3 Deployment
release notes.

Kept here deliberately, despite looking extractable: the status groupings and
retention text. `list_deployments` and `get_deployment` are shipped, so the
corrections read as generic — but they enumerate `staged`, `staging`,
`activating`, `reverting`, `extracting` and `loading`, while `main` defines only
`pending`, `installing`, `replicating`, `restarting`, `success`, `failed` and
`rolled_back`. Extracting them would publish statuses that do not exist. Same for
retention, which mixes the shipped payload bound with the unshipped staging one.
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-599

This preview will update automatically when you push new commits.

dawsontoth added a commit that referenced this pull request Aug 25, 2026
Extracts from #599 everything whose upstream is settled, now that harper#1849
has been **closed** — with #2135 and #2301, which were stacked on it — leaving no
two-phase surface in main at all.

**OIDC trusted publishing** (harper#2173, merged 2026-08-21, awaiting v5.3.0).
Unlike the two-phase work this is merged, so the API is fixed and only the release
is pending:

- `add_oidc_trust` / `list_oidc_trust` / `drop_oidc_trust` (super_user) and the
  unauthenticated `exchange_oidc_token`, with the replace-not-merge semantics.
- The GitHub Actions policy-specificity rules as a table — pin the repository, pin
  the workflow, gate the ref — with what each leaves open unsatisfied, since the
  ref gate is stricter than npm's model and is the rule most likely to refuse a
  real workflow.
- The `operations` scope, including that it covers the Operations API and SQL but
  **not** an application's REST/GraphQL path, so it bounds what CI can administer
  rather than what data it reaches (harper#2201 tracks that gap).
- Single-use tokens with an honest account of the cross-node replay race; one
  rejection message for every failure with the reason in the `oidc-trust` log;
  `drop_oidc_trust` not revoking already-issued tokens.
- The CLI exchange as precedence entry 7 — ranked below every configured
  credential, so adding `id-token: write` to a workflow that still sets a token
  secret does not silently change identity.

**Ops-table anchors.** Six user/role rows and seven SSH rows pointed at
`#certificate-management`, which documents neither. Repointed to `#users--roles`
and `#components`; the five genuine certificate rows are untouched. A pre-existing
docs bug with no upstream dependency, which is why it should not have been sitting
behind a feature branch.

**`AGENTS.md`** gains the rule that produced most of this stack's version churn:
derive a badge from core's git tags, not from a feature branch's `package.json`,
and re-check every refresh pass.

Deliberately left on #599: the deployment status groupings. They read as generic
tracking corrections, but they enumerate `staged`, `staging`, `activating`,
`reverting`, `extracting` and `loading` — and main's code defines only `pending`,
`installing`, `replicating`, `restarting`, `success`, `failed` and `rolled_back`.
Extracting them would document statuses that do not exist.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants