From 6c399af464d461efceb4a3246c3487347c85d042 Mon Sep 17 00:00:00 2001 From: Dawson Toth Date: Tue, 3 Nov 2026 09:31:08 -0500 Subject: [PATCH 1/6] Document per-PR preview deployments for 5.4 Lead the CI guide's preview recipe with harper preview setup, the starter workflow, the preview and preview-cleanup environments and HARPER_PREVIEW_TARGET, with a short note for 5.3. Document enable_previews, disable_previews and get_preview_policy, preview metadata and expiry on deploy_component, the component_deploy role grant, deployment.previews.*, harper preview, pattern credential grants, CORS wildcard origins, and what changed for isolated applications, payload deploys and OIDC exchanges. Co-Authored-By: Claude Opus 5.5 --- learn/developers/deploying-from-ci.mdx | 269 +++++++++++------- learn/developers/multiple-applications.mdx | 2 +- reference/cli/commands.md | 127 ++++++++- reference/components/applications.md | 22 +- .../components/javascript-environment.md | 2 +- reference/components/module-loading.md | 2 +- reference/configuration/options.md | 11 +- reference/http/configuration.md | 3 + reference/http/overview.md | 2 +- reference/operations-api/operations.md | 183 +++++++++++- reference/security/secrets.md | 6 + reference/users-and-roles/overview.md | 55 +++- release-notes/v5-lincoln/5.4.md | 37 +++ 13 files changed, 598 insertions(+), 123 deletions(-) diff --git a/learn/developers/deploying-from-ci.mdx b/learn/developers/deploying-from-ci.mdx index 7c9f28086..6f6166eb9 100644 --- a/learn/developers/deploying-from-ci.mdx +++ b/learn/developers/deploying-from-ci.mdx @@ -19,7 +19,8 @@ Both work with private sources: `deploy_component` accepts a [`credentials`](/re - A GitHub Actions workflow that deploys a tagged release straight from a repository - A GitHub Actions workflow that builds, publishes to GitHub Packages, and deploys the published artifact - How to wait until every node has taken the release, and how to go back to a previous one -- How to deploy isolated applications and keep one preview per PR, with shared databases and cleanup on closure +- How to deploy isolated applications +- How to give each pull request its own preview, on its own branch of the data, with one setup command and a starter workflow ## Prerequisites @@ -197,7 +198,7 @@ A few details worth noting: - **`harper@^5.3`** pins the CLI's major version. 5.3 is the first CLI that authenticates with the identity token, and rerunning an old tag after a new major release still installs a 5.x CLI. - **`by_ref=true`** deploys `git+https://github.com//.git#`, where the SHA is the commit the tag points to (`GITHUB_SHA`). Every node clones that exact commit, even if the tag is moved later. Pass `ref=`, with the full commit SHA, to deploy a different commit. In a private repository, a tag resolves only if the checkout fetched it, as `fetch-depth: 0` does: the CLI looks up anything else with `git ls-remote`, which has no credential once `persist-credentials: false` keeps the token out of `.git/config`. - **`replicated` isn't needed.** On Harper Fabric and Harper Pro, a deploy goes to every node in the cluster unless you pass `replicated=false`. Each node fetches and installs the package itself, resolving the credential from the replicated secrets store. Harper core on its own does not replicate. -- **`restart=rolling`** restarts nodes one at a time, so they never all restart at once. Each node stays in rotation while it restarts. `restart=true` deploys to every node at once; each node replaces its workers a few at a time while the others keep serving. [Choose a restart mode](../administration/safe-deployments-and-rollback.mdx#choose-a-restart-mode) compares the two. The [preview recipe](#per-pr-previews-with-shared-databases) also uses inline restarts on clusters, with an explicit allowance for concurrent initial restarts. On Harper core alone, a rolling job that only restarts — before v5.4.0, or when `certification` is `unavailable` — fails with `Replication not implemented`, which fails the wait step. +- **`restart=rolling`** restarts nodes one at a time, so they never all restart at once. Each node stays in rotation while it restarts. `restart=true` deploys to every node at once; each node replaces its workers a few at a time while the others keep serving. [Choose a restart mode](../administration/safe-deployments-and-rollback.mdx#choose-a-restart-mode) compares the two. [Per-PR previews](#per-pr-previews) use `restart=true`, which restarts only each preview's own worker. On Harper core alone, a rolling job that only restarts — before v5.4.0, or when `certification` is `unavailable` — fails with `Replication not implemented`, which fails the wait step. - **`json=true`** prints the deploy's result as JSON on stdout. Progress and authentication messages go to stderr, so `deploy.json` holds only the result. A deploy that fails exits non-zero, which fails the step. - **Waiting for every node** . With `restart=rolling` on v5.4.0 and later, unless the deploy's `certification` is `unavailable`, the other nodes take the release in a job, one at a time, and `harper deploy` [waits for them](/reference/v5/cli/commands#waiting-for-a-rolling-deploy) on the deployment record before it exits: the deploy step fails, naming each node that did not take the release, or passes once every node has. Its result then carries `rollout`, and the wait step does nothing. Otherwise, as before v5.4.0, `harper deploy` exits `0` once the node you called has taken the release and started the rolling job: every node has installed the release, and the job only restarts them; on v5.4.0 and later, a node that did not restart fails the job, and its `result.replicated` says which (see [how a rolling restart reports each node](/reference/v5/operations-api/operations#restart_service-replicated)). The wait step then polls the job's `restartJobId` with `get_job` until it ends `COMPLETE` or `ERROR`, and retries a poll that fails, such as one that reaches a node while it restarts. Each `harper get_job` authenticates through the trust policy the same way. It gives up after 60 polls, about ten minutes. Nodes restart one after another, so on a cluster with many nodes raise the count. With `restart=true` there is no job, and the deploy step itself fails when a node rejects the release, unless you pass `ignore_replication_errors=true`. - **The job lives on one node.** The deployment record that `harper deploy` waits on replicates, so its wait works through a load-balanced cluster URL. Harper does not replicate jobs, so only the node that received the deploy knows the rolling job, and the wait step's `get_job` polls have that limit. Any other node answers `get_job` with an empty list, which the wait step treats as not finished and polls again. So through a load-balanced cluster URL, at least one poll has to reach the node that received the deploy. Routing that keeps the runner on one node sends its polls where the deploy went, and routing that spreads requests across nodes reaches it within a few polls. Routing that sends the polls to a different node than the deploy never does: the step fails, saying the job was not on the node that answered and its outcome is unknown, even when the rollout succeeded. If your load balancer routes that way, use one node's own URL for both the deploy and the wait, and as the trust policy's `audience`. @@ -343,151 +344,213 @@ Keep the workflow's wait step when using `restart=rolling`. For a single-node in Creating an isolated application, or turning isolation on or off, also restarts the shared workers. Later redeploys that keep the application isolated restart only its worker, except an activation retry whose release is already live. A package redeploy that omits `isolated` preserves it; pass `isolated=false` to return the application to the shared pool. -## Per-PR previews with shared databases +## Per-PR previews - + + +A preview runs a pull request's code at its own URL, beside production, so a reviewer can try the change before it merges. PR `42`, from the branch `fix/cart`, deploys as `my-app-pr-42`, an [isolated application](/reference/v5/components/applications#isolated-applications) served at `https://fix-cart.app./`. Each push to the PR updates it. Closing or merging the PR drops it, and the cluster drops it on its own seven days after its last deploy, so a missed close event leaves nothing behind for long. By default, each preview runs on its own [branch of the cluster's databases](/reference/v5/components/applications#branched-databases), so what it writes stays out of production's data. -A preview can run each PR's code at its own hostname while sharing the cluster's databases. PR `42`, for example, deploys as `my-app-pr-42` at `pr-42.preview.example.com`; new commits replace that copy, and closing or merging the PR triggers cleanup. +It takes one command and one workflow file, and stores no secret in GitHub: -**The preview shares data with every application using those databases, including production if it runs on the same cluster.** Its writes, schema changes, and application startup work affect that shared data. Instance-wide roles declared by the PR also apply, and background work can run once per preview. Isolation separates worker state and exported routes; it does not sandbox code or isolate database access. This recipe is for trusted PRs from branches in the same repository. +- [`harper preview setup`](/reference/v5/cli/commands#harper-preview-setup) enables previews of the application on the cluster. It creates a user whose role can deploy and drop only that application's previews, and trust policies that let one workflow file act as that user. +- The workflow, `.github/workflows/preview.yaml`, runs on `pull_request_target` from your default branch. It runs `harper preview deploy` when a PR opens or changes, and `harper preview drop` when it closes. -### Prepare routing and preview authentication +Read [How previews stay safe](#how-previews-stay-safe) before you turn previews on for a cluster that also serves production. -Choose a hostname suffix such as `preview.example.com`. For each preview, provision DNS, TLS, and routing for `pr-.`: +:::note Coming soon -- **Fabric:** add, verify, and bind each hostname as a custom domain. The workflow below deploys the application; it does not register domains. Wildcard-domain automation is still tracked under [PR preview deployments](https://github.com/HarperFast/harper/issues/3042), so a wildcard DNS record alone is not sufficient. -- **Self-managed:** configure your proxy to route each hostname to the corresponding application's socket. For `my-app-pr-42` and secure port `9927`, the HTTP socket is `/sockets/app-my%2Dapp%2Dpr%2D42-9927.sock`. That socket serves plain HTTP; the proxy terminates TLS. +Two more ways to run the same previews are on the way, and neither is released yet: -On GitHub.com, create a separate `preview` GitHub environment and OIDC policy instead of reusing the production policy. This workflow uses `pull_request_target`, runs from the trusted default branch, and does not check out or execute PR code on the runner. In **Settings > Environments > preview**, allow `main` under **Deployment branches and tags**, and, when sharing a production cluster, require deployment reviewers if your repository's GitHub plan supports them. Without that approval gate, every same-repository writer and automation that opens a PR can run code with access to production data. Required reviewers also gate closure and retargeting cleanup: approve those runs, or the preview remains running. Every same-repository PR closure can request that approval, even if the PR never had a preview. Before closing a previewed PR, finish or cancel any older run waiting for approval so per-PR concurrency can admit cleanup. Replace `main` throughout this example if your default branch has another name. See [GitHub environment rules](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments) and [the `pull_request_target` event](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#pull_request_target). +- **Harper Studio's Previews panel** turns previews of an application on and off, and shows its limits, from the browser. +- **[`HarperFast/deploy-action`](https://github.com/HarperFast/deploy-action)** is a GitHub Action that runs the same preview lifecycle as a workflow step, and also comments each preview's URL on its PR. -Ensure your Actions execution policy allows `pull_request_target` for `.github/workflows/preview.yml`. GitHub's default protection will block that event for affected public repositories starting November 2, 2026; private and internal repositories are unaffected. See [GitHub's execution-protection rollout](https://github.blog/changelog/2026-09-17-workflow-execution-protections-in-github-actions-generally-available/). +Both use the cluster-side setup described here. Until they are released, use the steps below. + +::: -Cleanup needs a **direct Operations API URL for every node**, rather than a balanced cluster URL. Put these URLs in the `HARPER_PREVIEW_NODE_TARGETS` repository variable as a JSON array, including the port and trailing slash. On a single-node instance, the array contains its one URL. On Fabric, arrange direct instance access with your administrator or support before enabling this recipe; a cluster URL alone cannot safely check each node. Keep the list current when cluster membership changes. +### Before you start -From your machine, logged in as a `super_user`, create a role that can deploy and clean up previews: +- **Every node runs v5.4.0 or later.** [On v5.3](#previews-on-v53) has what differs before that. +- **The cluster can run isolated applications**: a nonzero `threads.count`, `http.securePort`, and `tls.unixDomainSockets: true`. Each running preview takes a slot of [`threads.maxIsolated`](/reference/v5/configuration/options#threads), `8` by default, on each node, and a worker thread with its memory. See [Requirements and refusals](/reference/v5/components/applications#requirements-and-refusals). +- **Branched data needs RocksDB storage**, and a module loader other than `native`. On a cluster that cannot branch, or for previews that should use the cluster's own data, set previews up with `dataMode=shared`, and read [Shared or branched data](#shared-or-branched-data) first. +- **Preview hosts reach the cluster.** Every Colocated and Dedicated cluster on Harper Fabric serves `*.app.` with no setup: see [Preview Hosts](/fabric/cluster-creation-management#preview-hosts), including how a cluster created before preview hosts gains them. On a self-managed cluster, point a wildcard DNS record and certificate for a preview domain at a proxy that routes each host to its application's socket, as in [Reaching an isolated application](/reference/v5/components/applications#reaching-an-isolated-application), and pass that domain to setup with `domain=`. +- **On your machine:** the Harper CLI, `super_user` access to the cluster, and the [`gh` CLI](https://cli.github.com/) signed in to an account that can manage the repository's Actions variables. + +### Enable previews + +From a checkout of the repository, on its default branch, logged in to the cluster as a `super_user`: ```bash -harper add_role role=ci_preview permission='{"operations":["deploy_component","drop_component","get_components","system_information"]}' -harper add_user username=ci-preview role=ci_preview active=true password="$(openssl rand -base64 32)" -harper add_oidc_trust \ - id=my-app-preview \ - issuer=https://token.actions.githubusercontent.com \ - audience=https://my-cluster.example.com:9925/ \ - user=ci-preview \ - claims='{"repository_id":"67890","workflow_ref":"my-org/my-app/.github/workflows/preview.yml@refs/heads/main","environment":"preview","event_name":"pull_request_target"}' -gh variable set HARPER_PREVIEW_DOMAIN --repo my-org/my-app --body preview.example.com -gh variable set HARPER_PREVIEW_NODE_TARGETS --repo my-org/my-app \ - --body '["https://node-a.example.com:9925/","https://node-b.example.com:9925/"]' +harper login https://my-cluster.example.com:9925 +harper preview setup app=my-app ``` -Use your repository id, workflow path, and cluster URL, as in the [release policy](#add-a-trust-policy). Add a trust policy for each node URL as well, with a unique `id`, that exact URL as `audience`, and the same user and claims. On Pro and Fabric, policies, users, and roles replicate, so you can create these policies from any node. On independent instances, authenticate as a `super_user` at each node and repeat the setup with `target=`. Ensure each node has the preview user, role, and matching policy. The CLI requests a token for the target it calls, so a policy with only the cluster URL as its audience does not authorize node URLs. The workflow uses the existing `HARPER_CLI_TARGET` variable. The preview role's operations apply to the whole cluster, not just projects whose names begin `my-app-pr-`; trust the people allowed to run this workflow accordingly. `get_components` can expose component configuration and files, so keep its response out of logs and artifacts. +`app` is the application's component name: the `project` it deploys as. Setup checks the GitHub side, then enables previews on the cluster, then writes the GitHub side: + +1. It looks the repository up for its id and default branch. +2. It offers to write the starter workflow to `.github/workflows/preview.yaml`, or, if that file exists, checks it and refuses one that could give the preview identity to a fork or to a PR's code. +3. It calls [`enable_previews`](/reference/v5/operations-api/operations#enable_previews) on the cluster, which creates: + - the role and user `my-app-ci-preview`, which can deploy, drop and list `my-app-pr-` previews and nothing else; + - the trust policy `github-actions-my-app-preview`, for jobs in the `preview` environment; + - the trust policy `github-actions-my-app-preview-cleanup`, for jobs in the `preview-cleanup` environment, whose token can only drop. +4. It writes the workflow, if you said to, and sets the repository variable `HARPER_PREVIEW_TARGET` to the cluster's URL. + +Commit the workflow to the default branch. GitHub runs a `pull_request_target` workflow from there, not from the PR. + +A few options change what setup enables. [`harper preview setup`](/reference/v5/cli/commands#harper-preview-setup) lists them all: + +- **`credential=true`**, for a private repository, which setup offers when it sees one. It seals a GitHub token for the cluster to fetch each PR's commit with. See [Private repositories](#private-repositories). +- **`maxPreviews=`** caps how many previews of the application each node keeps live. A deploy past the cap is refused, so close PRs you are done with. +- **`maxTtlDays=`** is the longest a preview lives after its last deploy, `7` by default. +- **`dataMode=shared`** lets previews use the cluster's databases rather than requiring a branch of them. +- **`domain=`** sets the preview domain, for a self-managed cluster or one you reach through a custom domain. -### Deploy on PR updates and remove on closure +The cluster holds these limits in the preview role, and enforces them on every deploy, so a change to the workflow cannot raise them. -This example is Path A for an application that runs from source. Add `.github/workflows/preview.yml` to your repository: +### Create the GitHub environments + +In the repository's **Settings > Environments**, create `preview` and `preview-cleanup`. On each, set **Deployment branches and tags** to the default branch only. GitHub creates an environment a workflow names with no rules at all, so create them before the first PR. + +- On **`preview`**, add required reviewers if the cluster also serves production and your GitHub plan supports them. Every deploy then waits for someone to approve the PR's code before it runs on the cluster. +- Leave **`preview-cleanup`** without reviewers. Its token can only drop previews, and a reviewer there would keep a closed PR's preview running until someone approves. + +### Public repositories: allow `pull_request_target` + +From December 2, 2026, GitHub blocks `pull_request_target` by default in public repositories that have no Actions event policy of their own; it already did so during brownouts on November 9 and 16, 2026. Private and internal repositories are not affected. See [GitHub's announcement](https://github.blog/changelog/2026-11-02-pull_request_target-blocked-by-default-december-2). + +In a public repository, allow the event for the preview workflow alone. In **Settings > Actions > Policies**, create a policy: + +1. Set its enforcement status to **Active**. +2. Under its target workflows, add `.github/workflows/preview.yaml`. +3. Turn on **Restrict events**, and allow `pull_request_target`. + +An organization or enterprise policy can still block the event. See [Control workflow execution](https://docs.github.com/en/actions/how-tos/administer/control-workflow-execution). + +### The preview workflow + +This is the workflow setup writes for `app=my-app`, on a repository whose default branch is `main`: ```yaml -name: Preview on Harper +# Written by `harper preview setup`. +name: Preview on: pull_request_target: - types: [opened, reopened, synchronize, edited, closed] - + types: [opened, reopened, synchronize, closed] + branches: ['main'] +permissions: {} +env: + HARPER_CLI_TARGET: ${{ vars.HARPER_PREVIEW_TARGET }} + PR: ${{ github.event.pull_request.number }} jobs: - preview: - if: >- - github.event.pull_request.head.repo.full_name == github.repository && - (github.event.action == 'closed' || - (github.event.action == 'edited' && github.event.changes.base) || - (github.event.action != 'edited' && github.event.pull_request.base.ref == 'main')) + deploy: + if: github.event.action != 'closed' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest environment: preview - timeout-minutes: 20 permissions: - contents: read - pull-requests: read id-token: write + pull-requests: read concurrency: - group: harper-preview-${{ github.repository }}-${{ github.event.number }} - cancel-in-progress: false + group: harper-preview-${{ github.event.pull_request.number }} env: - HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }} - PREVIEW_DOMAIN: ${{ vars.HARPER_PREVIEW_DOMAIN }} - PREVIEW_NODE_TARGETS: ${{ vars.HARPER_PREVIEW_NODE_TARGETS }} - PR_NUMBER: ${{ github.event.number }} + BRANCH: ${{ github.event.pull_request.head.ref }} + SHA: ${{ github.event.pull_request.head.sha }} + GH_TOKEN: ${{ github.token }} steps: - uses: actions/setup-node@v7 with: - node-version: 26 - - run: npm install --global harper@^5.3 - - name: Update or remove the preview - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] - PROJECT="my-app-pr-$PR_NUMBER" - PR=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER") - jq -e 'type == "array" and length > 0 and all(.[]; type == "string" and test("^https?://"))' <<< "$PREVIEW_NODE_TARGETS" > /dev/null - if [ "$(jq -r .state <<< "$PR")" = closed ] || - [ "$(jq -r .base.ref <<< "$PR")" != main ]; then - cleanup_node() { - export HARPER_CLI_TARGET="$1" - harper get_components json=true > components.json || return 1 - harper system_information attributes='["threads"]' json=true > system.json || return 1 - jq -e '.error == null and (.entries | type == "array")' components.json > /dev/null || return 1 - jq -e '.threads | type == "array"' system.json > /dev/null || return 1 - SHOULD_RESTART=$(jq -r --arg project "$PROJECT" 'any(.threads[]; .application == $project)' system.json) || return 1 - if jq -e --arg project "$PROJECT" 'any(.entries[]; .name == $project)' components.json > /dev/null || - [ "$SHOULD_RESTART" = true ]; then - harper drop_component project="$PROJECT" restart="$SHOULD_RESTART" replicated=false json=true > drop.json || return 1 - jq -e 'type == "object" and .error == null' drop.json > /dev/null || return 1 - harper get_components json=true > after-components.json || return 1 - harper system_information attributes='["threads"]' json=true > after-system.json || return 1 - jq -e --arg project "$PROJECT" '.error == null and (.entries | type == "array") and (any(.entries[]; .name == $project) | not)' after-components.json > /dev/null || return 1 - jq -e --arg project "$PROJECT" '(.threads | type == "array") and (any(.threads[]; .application == $project) | not)' after-system.json > /dev/null || return 1 - fi - } - FAILED=0 - while IFS= read -r NODE_TARGET; do - if ! cleanup_node "$NODE_TARGET"; then - echo "Preview cleanup failed: $NODE_TARGET" >> "$GITHUB_STEP_SUMMARY" - FAILED=1 - fi - done < <(jq -r '.[]' <<< "$PREVIEW_NODE_TARGETS") - exit "$FAILED" + node-version: 24 + - run: npm install --global harper@^5.4 + - run: | + pr() { gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq '.state + " " + .head.sha'; } + before=$(pr) + if [ "$before" = "open $SHA" ]; then + harper preview deploy app=my-app pr="$PR" branch="$BRANCH" by_ref=true ref="$SHA" || deployed=$? + # Closed while this deployed: its cleanup may have run first. + after=$(pr) + if [ "${after%% *}" = closed ]; then harper preview drop app=my-app pr="$PR"; fi + exit "${deployed:-0}" fi - SHA=$(jq -r .head.sha <<< "$PR") - HOST="pr-$PR_NUMBER.$PREVIEW_DOMAIN" - harper deploy project="$PROJECT" by_ref=true ref="$SHA" \ - isolated=true host="$HOST" restart=true json=true > deploy.json - echo "Preview: https://$HOST" >> "$GITHUB_STEP_SUMMARY" + cleanup: + if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + environment: preview-cleanup + permissions: + id-token: write + pull-requests: read + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/setup-node@v7 + with: + node-version: 24 + - run: npm install --global harper@^5.4 + - run: | + # A deploy still holding the preview answers 409 until it finishes; a reopened PR keeps its preview. + for attempt in 1 2 3 4 5; do + if state=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .state); then + [ "$state" = closed ] || exit 0 + harper preview drop app=my-app pr="$PR" && exit 0 + fi + sleep 30 + done + exit 1 ``` -The workflow reads the PR's current state, base branch, and head SHA from GitHub, rather than deploying the default-branch commit in `GITHUB_SHA`. Its concurrency group serializes updates and cleanup for that PR without canceling a deploy midway. A delayed run sees a closed or retargeted PR and cleans up instead of recreating the preview. The job filter ignores title/body edits and unrelated updates on other base branches, while admitting closures and base-branch edits. The fresh API read remains authoritative. +- **It never checks out the PR.** `by_ref=true ref="$SHA"` tells the cluster to fetch the PR's head commit itself, so no step of a job holding the identity token runs the PR's code. The `HARPER_PREVIEW_TARGET` variable becomes `HARPER_CLI_TARGET`, and every `harper` command authenticates through the trust policy, with no stored credential. +- **Fork PRs are skipped.** Each job runs only when the PR's head repository is this repository. +- **A deploy that waited goes ahead only while it is still current.** The job deploys only while the PR is open at the commit the event named, since an approval on `preview` can hold it for a while. If the PR closes while it deploys, it drops what it deployed. The concurrency group runs one deploy per PR at a time. +- **Cleanup runs in its own environment**, with a token that can only drop, so it never waits for the `preview` environment's reviewers. While a deploy of the same PR is still activating, the drop answers `409`, so the job retries for two minutes. A reopened PR keeps its preview. +- **`harper@^5.4`** installs a 5.x CLI that is v5.4.0 or later. -Cleanup checks component files and running workers separately on **each listed node**, so it also handles a worker left running after files were removed. An absent node is skipped: dropping an absent preview with `restart=true` can restart its shared workers. Each necessary drop uses `replicated=false`, and requests a restart only when that node has the preview worker. Files without a worker are dropped with `restart=false`; the node's shared workers stay running. It then verifies the component and worker are both absent; a successful drop response alone is insufficient. The loop attempts the remaining nodes after an error and fails the job with each failed target in the summary. After a failed, timed-out, or interrupted run, rerun cleanup and check those nodes directly with an administrator account; do not reuse the project name until cleanup has completed on every node. An omitted node cannot be checked or cleaned by this workflow. +Setup checks the workflow only when it runs, so keep its rules when you edit the file later: no checkout or fetch of the PR's code in a job with `id-token: write`, and the same-repository condition on every job that holds the token. The cluster still enforces the role's limits, whatever the workflow does. -The deploy still propagates to peers by default on Fabric and Pro, and a failed peer install fails the CLI unless `ignore_replication_errors` is set. Do not enable that option here. Cleanup uses direct node calls specifically to recover from asymmetric deploy or drop state. +### Managing previews -`restart=true` waits for the restart on the answering node, but a successful response does not establish that every peer loaded the application: a peer restart can be slow or fail, and a node can refuse isolation. This workflow prints the URL but does not probe preview health. Check [application status](/reference/v5/operations-api/operations#set_status--get_status--clear_status) on each node with an administrator account and verify the routed preview endpoint. This recipe uses inline restarts instead of the production workflows' rolling job. **The first deploy of each preview can restart shared workers on all nodes concurrently**, disrupting existing connections. Use a dedicated preview cluster if that interruption is unacceptable; adapting the [rolling workflow](#path-a-deploy-a-tagged-release-from-your-repository) also requires granting `get_deployment` and `get_job` to the preview role and retaining its wait step. +As a `super_user`, or as the preview user: -Fork PRs are skipped; keep that same-repository condition. The policy pins `workflow_ref` so a PR cannot change the trusted workflow to mint a token with different commands; `event_name` explicitly authorizes `pull_request_target`, which Harper otherwise refuses unless constrained. Provision the hostname before opening the preview URL; successful deployment alone does not make an unconfigured hostname reachable. Close unused previews to release worker slots. Dropping a running isolated preview stops its worker and normally reclaims retained installs; inspect node logs if storage cleanup fails. Removing an application does not undo its shared-database writes or remove DNS and Fabric custom-domain registrations; clean up that routing separately. +```bash +harper preview list app=my-app # each preview: PR, branch, commit, URL, expiry +harper preview drop app=my-app pr=42 +``` -### Private sources and built artifacts +- **Expiry.** Each deploy sets the preview to expire `ttl` days later, 7 by default and never more than the application's `maxTtlDays`. One node drops each expired preview on every node, checking every [`deployment.previews.sweepInterval`](/reference/v5/configuration/options#deployment). +- **How it is set up.** [`get_preview_policy`](/reference/v5/operations-api/operations#get_preview_policy) returns the application's identity, policies and limits, and in `problems`, anything on the cluster that differs from what `enable_previews` wrote. +- **A failed preview.** A preview that fails to load in its [canary worker](/reference/v5/operations-api/operations#certifying-a-release-in-a-canary-worker) fails the deploy, and the job's log says why. +- **Turning previews off.** `harper preview teardown app=my-app` revokes the identity, drops every preview of the application on every node, and removes the user, role and trust policies. It leaves the workflow file and the repository variable for you to remove. -For a private repository, each preview project needs its own grant to a durable source credential. Once you know the PR number, run setup from your machine for that project's name, using a dedicated read-only preview token: +### Private repositories -```bash -harper deploy setup=true provider=github project=my-app-pr-42 -``` +The cluster fetches each PR's commit itself, so for a private repository it needs a GitHub token that lasts longer than the workflow run. `harper preview setup credential=true` asks for one, seals it on your machine with the cluster's public key, and grants it to every preview of the application at once, as the secret `deploy.my-app-pr-_.git.github.com`. The starter workflow it writes then deploys with `credential=true`. + +Use a token for this alone: a [fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with **Contents: read-only** on this repository. Not the token production deploys with. Harper uses it only to install each preview; the previews' own code cannot read it. To rotate it, run `harper deploy setup=true provider=github project='my-app-pr-*'` with the new token. + +### Applications that need a build + +The starter workflow deploys by reference, so every node installs the PR's commit as it is in the repository. That suits an application that runs from source. Don't add a build step to the preview workflow: any step in a job that holds the identity token could use it, and a build runs the PR's code and its dependencies. + +To preview an application that needs a build, build and publish each PR's commit from a separate `pull_request` workflow, which gets no identity token, and deploy the published package from the preview job with `package=` in place of `by_ref=true ref="$SHA"`. GitHub starts both workflows at once, and the trust policies match only `pull_request_target`, so the preview job has to wait for that build itself. + +### How previews stay safe + +- **The workflow is trusted, not the PR.** The workflow runs on `pull_request_target`, from the default branch, and never runs the PR's code on the runner. The trust policies pin its `workflow_ref` to the default branch, `event_name` to `pull_request_target`, and its environment, so a PR that edits the workflow cannot change what runs with the identity: its edits take effect only once it merges. +- **Forks get nothing.** Each job runs only for a PR from a branch of the same repository. A fork's PR never gets the identity, and no preview. +- **The PR's code still runs on the cluster.** The preview role can deploy and drop only `my-app-pr-`, as an isolated application under the preview domain, within the application's limits, with no table permissions. Harper does not sandbox an application's code, though: a preview runs in the Harper process like any application, and can reach what any application can. So anyone who can open a PR from a branch of the repository can run code on the cluster. Required reviewers on the `preview` environment are the gate for that. +- **Previews share a site with production.** A preview's host is under the cluster's own host name, so a browser treats it as the same site as anything production serves from that host name. A preview can set cookies production receives, and `SameSite` does not keep them apart: see [Previews share a site with production](/fabric/cluster-creation-management#previews-share-a-site-with-production) for `__Host-` cookie names and CSRF or `Origin` checks. What production serves only from a custom domain is a different site. +- **Revoking.** `harper preview teardown` stops new runs. A token already issued lasts an hour; deactivate the user to stop it sooner: `harper alter_user username=my-app-ci-preview active=false`. + +#### Shared or branched data + +With the default `dataMode=branched`, the cluster refuses a preview deploy that does not branch, and each preview gets a private copy of every database except `system`, taken when it first loads. Its writes, and the tables and schema changes it makes, stay in its copy, which is removed when the preview is dropped. Some things are still shared: -Then add `credential=true` to the workflow's deploy command. It derives `deploy.my-app-pr-42.git.github.com` for PR `42`. A secret granted only to `my-app` does not authorize `my-app-pr-42`; the production setup cannot be reused just by changing `project`. The `opened` run may happen before this setup and fail; provision the credential, then rerun the workflow. Preview code can read secrets granted to its component, so do not grant it the production source token. Rotation must update each active preview project's secret row. Dropping the component does not delete that row: after cleanup, an administrator should remove `deploy.my-app-pr-42.git.github.com` with [`delete_secret`](/reference/v5/operations-api/operations#delete_secret), including its grants. Reopening after secret deletion needs setup again. Credential provisioning, rotation, and deletion remain outside this workflow. +- **The `system` database**, users and roles, cannot be branched. Roles a preview declares apply to the whole cluster. +- **A database created after the preview loads** is not branched. +- **Work outside the database**, such as calls to other services, or files written outside the application's own directory, happens once per preview. -For applications that need a build, build and publish an immutable package version for each PR commit using Path B's delivery model, and trigger the trusted deploy only after publication finishes. Keep PR builds in a separate workflow; do not check out or run PR code in this `pull_request_target` job. Replace `by_ref=true ref="$SHA"` with `package="@my-org/my-app@"`, retaining `project`, `host`, `isolated=true`, and cleanup. Set up the registry credential for each preview project and pass its `credentials` reference, as in Path B. A payload upload cannot set `isolated=true`. +With `dataMode=shared`, previews read and write the cluster's databases, the ones production uses if it runs on the same cluster. A preview's writes, schema changes and startup work then change production's data. Use it only for an application whose data a preview may change, or on a cluster for previews alone. -### Adding private database forks later +### Previews on v5.3 -The recipe deliberately omits `branchedDatabases`. On v5.3.0 and v5.3.1, a package deployment ignores that key when loading the application ([harper#3071](https://github.com/HarperFast/harper/issues/3071)). Follow [Branched databases](/reference/v5/components/applications#branched-databases) for current behavior and limitations; database forks can be added to the recipe once that deployment path is fixed. Revisit cleanup then as well: a drop with `restart=false` leaves fork storage in place. +None of the above exists before v5.4.0. On v5.3, a preview needs a role, user, trust policy and workflow written by hand, and that role can deploy and drop any component on the cluster. On Fabric each preview host is a custom domain to add, verify and bind; a payload deploy cannot be isolated or branched; and cleanup has to call every node itself. The v5.3 recipe is in [the previous version of this guide](https://github.com/HarperFast/documentation/blob/3d562271/learn/developers/deploying-from-ci.mdx#per-pr-previews-with-shared-databases). Run v5.3 previews on a cluster for previews alone, or behind required reviewers when they share a cluster with production. ## Other CI systems diff --git a/learn/developers/multiple-applications.mdx b/learn/developers/multiple-applications.mdx index c91be4543..af38e4f52 100644 --- a/learn/developers/multiple-applications.mdx +++ b/learn/developers/multiple-applications.mdx @@ -152,7 +152,7 @@ listings-api-staging: The first time the application loads, Harper takes a snapshot of each named database as the application's fork. The `databases` the application imports from `harper` then resolves those names to its fork, so its code is unchanged, and every other application keeps using the base. The `tables` import follows only a fork of the default `data` database, so reach any other branched database, like `listings` here, through `databases`. The fork is durable and is never refreshed from the base. It is not replicated: each node of a cluster keeps its own. `drop_component` with `restart=true` removes it. -A fork is still not an enforced boundary. The bare `databases` and `tables` globals reach the base. The application can also still open any database that it did not branch. On v5.3.0 and v5.3.1, an application deployed by `package` or `by_ref` gets no fork at all ([harper#3071](https://github.com/HarperFast/harper/issues/3071)). See [Branched databases](/reference/v5/components/applications#branched-databases) for the requirements and the manual steps that work in the meantime. +A fork is still not an enforced boundary. In the shared workers, under the default module loader, the bare `databases` and `tables` globals reach the base; from v5.4.0 they reach the fork in an isolated application. The application can also still open any database that it did not branch. On v5.3.0 and v5.3.1, an application deployed by `package` or `by_ref` gets no fork at all ([harper#3071](https://github.com/HarperFast/harper/issues/3071)). See [Branched databases](/reference/v5/components/applications#branched-databases) for the requirements and the manual steps that work in the meantime. When a boundary has to hold against code you do not fully trust, or against a security or compliance requirement, put the application on a separate instance or cluster. That is the only boundary Harper enforces for data. diff --git a/reference/cli/commands.md b/reference/cli/commands.md index 640a68880..680348329 100644 --- a/reference/cli/commands.md +++ b/reference/cli/commands.md @@ -153,8 +153,10 @@ All parameters are passed as `key=value` arguments. Every parameter is optional. - `deployment_timeout=` - How long, in milliseconds, a peer waits to receive the replicated payload before failing (default: `120000`). (Added in: v5.1.4) - `ignore_replication_errors=true` - Treat a peer that fails to receive the deploy as non-fatal instead of failing the whole operation. (Added in: v5.1.4) - `force=true` - Allow deploying over a protected core component name. -- `urlPath=` - HTTP path the component is mounted at (e.g. `/api/v2`). Requires `package`. -- `host=` - Virtual hostname the component is served on (e.g. `api.example.com`). Requires `package`. (Added in: v5.2.0) +- `urlPath=` - HTTP path the component is mounted at (e.g. `/api/v2`). Before v5.4.0, requires `package`. +- `host=` - Virtual hostname the component is served on (e.g. `api.example.com`). Before v5.4.0, requires `package`. (Added in: v5.2.0) +- `isolated=true` - Run the application in a worker thread of its own. See [Isolated applications](../components/applications.md#isolated-applications). Before v5.4.0, requires `package`. (Added in: v5.3.0) +- `branchedDatabases=''` - Give the application a private branch of these databases, as a JSON list, or `true` for all of them. See [Branched databases](../components/applications.md#branched-databases). (Added in: v5.3.0) - `credentials=''` - JSON array of credential objects for installing from a private npm registry or git repository. See [Private deploy sources](#private-deploy-sources). (Added in: v5.2.0) - `activate=false` - Build and install the release on each node without making it live. The result's `deployment_id` names the staged build. Cannot be combined with `restart`. See [Staging a build and activating it later](../operations-api/operations.md#staging-a-build-and-activating-it-later). (Added in: v5.3.0) - `deployment_id=` - Make a staged build live, or go back to a release a later deploy replaced, with no rebuild or reinstall. Takes no `package`, `by_ref`, or other build inputs. Pass `restart` here rather than on the stage. See [Going back to a previous release](../operations-api/operations.md#going-back-to-a-previous-release). (Added in: v5.3.0) @@ -265,6 +267,8 @@ This interactive flow prompts for the provider (a private GitHub repository or a See [`deploy_component` credentials](../operations-api/operations.md#deploy-credentials-credentials) for the full credential-entry shape, including passing a literal `token` instead of a sealed `secret` reference. + With `provider=github` or `provider=npm`, `project` can be a pattern ending in `*`, which grants one credential to every matching component, such as every preview of an application: `harper deploy setup=true provider=github project='my-app-pr-*'`. The secret's name has `_` in place of `*`, as in `deploy.my-app-pr-_.git.github.com`, and a deploy references it by that name with `credentials`, as [`harper preview deploy credential=true`](#harper-preview-deploy) does; `credential=true` on `harper deploy` would derive the one component's name instead. + #### GitHub Actions deploys (OIDC) @@ -289,7 +293,7 @@ It needs no secret custody, and it does this: 4. Reads the policy back and compares it, claim by claim, with what it meant to write. 5. Sets the repository's `HARPER_CLI_TARGET` variable with `gh`, if the repository doesn't have one. One that already names this cluster is left as it is. One that names another cluster is left too, since every workflow in the repository that reads it deploys there: setup prints the command to change it and exits non-zero. Without `gh`, or if the variable can't be read or written, it prints the command to run and exits non-zero. -It only creates. Running it again leaves the cluster and the variable unchanged, and a role, user or policy that already exists but differs, or that someone deactivated or disabled, stops it before it writes, naming what differs. To replace a policy, drop it with `harper drop_oidc_trust id=github-actions-` and run setup again. Because `add_oidc_trust` replaces a policy, setup checks again just before writing it, but a policy with the same id that another super user creates in that moment, enabled or not, would still be overwritten. Setup never writes a policy that already exists, so it can't undo a disable or a drop. +It only creates. Running it again leaves the cluster and the variable unchanged, and a role, user or policy that already exists but differs, or that someone deactivated or disabled, stops it before it writes, naming what differs. To replace a policy, drop it with `harper drop_oidc_trust id=github-actions-` and run setup again. Because `add_oidc_trust` replaces a policy, setup checks again just before writing it, but a policy with the same id that another super user creates in that moment, enabled or not, would still be overwritten. Setup never writes a policy that already exists, so it can't undo a disable or a drop. It also stops, before and again just before it writes, when another enabled policy could match the same runs and give them a different user or operations, since [the exchange refuses](../operations-api/operations.md#exchange_oidc_token) a token two policies disagree on: give the project its own workflow (`workflow=`) or environment (`environment=`), or drop the other policy. If the variable wasn't set, the cluster part is already done: run the printed `gh variable set` command, or run setup again, which leaves the cluster as it is and tries the variable again. @@ -297,6 +301,123 @@ The deploy user can deploy any component on the cluster, not only this project, The policy pins one branch, so it suits a workflow that deploys when changes merge. A workflow that deploys on tags needs a policy written by hand: see [Deploying from a CI/CD Pipeline](/learn/developers/deploying-from-ci#add-a-trust-policy). +### `harper preview` + + + +Runs a preview of an application for each pull request. `setup` and `teardown` run once per application, from your machine. `deploy`, `drop` and `list` run for each PR, usually from the workflow `setup` writes. [Per-PR previews](/learn/developers/deploying-from-ci#per-pr-previews) walks through the whole flow. + +A preview of the application `my-app` for PR `42` is the [isolated application](../components/applications.md#isolated-applications) `my-app-pr-42`. It is served at `.`, where the slug comes from the PR's head branch, and on a cluster that supports it, it runs on its own [branch of the databases](../components/applications.md#branched-databases). The cluster drops it when the PR closes, or when it expires. + +#### `harper preview setup` + +Enables previews of one application on the cluster, and sets up the GitHub side. Run it once, signed in to the cluster as a `super_user` (`harper login`), from a checkout of the application's repository: + +```bash +harper preview setup app=my-app target=https://my-cluster.example.com:9925 +``` + +It checks everything it can before it changes anything, and a refusal before step 6 ends with `Nothing was changed.` + +1. Takes the GitHub repository from the checkout's `origin` remote, or `repo=/`, and looks it up with the `gh` CLI or GitHub's public API for its numeric id, its default branch, and the spelling of its name that GitHub uses in the token. When neither answers, pass `repository_id=`. +2. Reads the preview workflow, `.github/workflows/preview.yaml`, from the checkout. It refuses a workflow the trust policies could never match, and one that may give the preview identity to a fork or to a PR's own code: see [What setup checks in the workflow](#what-setup-checks-in-the-workflow). If the file doesn't exist, it offers to write the starter workflow (`write_workflow=true` without a prompt). When it can neither check nor write the workflow, such as when it is not run from a checkout of the repository, it stops unless you pass `allow_unverified=true`. +3. For a private repository, offers to seal a credential for the cluster to fetch each PR with (`credential=true`). +4. Reads the repository's `HARPER_PREVIEW_TARGET` variable. One that names another cluster stops setup. One it cannot read, such as when `gh` is not installed, stops it unless you pass `allow_unverified=true`, and you then set the variable yourself afterward. +5. With `credential=true`, asks for the token and checks that the cluster can hold a sealed secret. +6. Calls [`enable_previews`](../operations-api/operations.md#enable_previews), which creates the role, the user and the two trust policies, and stores the application's preview limits. +7. With `credential=true`, seals the token on your machine and grants it to every preview of the application, as the secret `deploy.-pr-_.git.github.com`. +8. Writes the starter workflow, if step 2 said to. Commit it to the default branch: GitHub runs `pull_request_target` workflows from there. +9. Sets `HARPER_PREVIEW_TARGET` to the cluster's URL with `gh`, if the repository doesn't have it. + +`enable_previews` only creates. Running setup again with the same arguments leaves the cluster as it is, and one whose records differ from what is already there stops with a `409` that names each difference. To change the limits, run `harper preview teardown`, then setup again. + +| Parameter | Default | Description | +| --------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `app=` | required | The application to preview. Its previews deploy as `-pr-`. | +| `target=` | your login's target | The cluster. Its URL, with port and trailing slash, is the trust policies' `audience` and the value of `HARPER_PREVIEW_TARGET`. | +| `repo=/` | the checkout's `origin` | The GitHub repository. | +| `repository_id=` | looked up | The repository's numeric id, for when setup can't look it up (`gh api repos// --jq .id`). | +| `branch=` | the repository's default branch, else `main` | The branch the workflow runs from. The trust policies pin the workflow at this branch. | +| `workflow=` | `.github/workflows/preview.yaml` | The workflow file, a `.yml` or `.yaml` file in `.github/workflows/`. | +| `environment=` | `preview` | The GitHub environment of the deploy job. The cleanup job's is always `preview-cleanup`. | +| `domain=` | `app.` | The domain every preview host sits one label under. Required when the target is an IP address or `localhost`. | +| `dataMode=branched\|shared` | `branched` | `branched` requires each preview to run on its own branch of the databases. `shared` lets previews use the cluster's databases, and the starter workflow then deploys with `branchedDatabases=false`. | +| `maxPreviews=` | `deployment.previews.maxPerApplication` | How many previews of the application each node keeps live at once. A deploy past it is refused. Without it, the node's [`deployment.previews.maxPerApplication`](../configuration/options.md#deployment) applies, which has no default. | +| `maxTtlDays=` | `7` | The longest a preview may live after its last deploy. A later expiry is lowered to it. | +| `credential=true` | asked, for a private repository | Seal a GitHub token for the cluster to fetch each PR's commit with. | +| `write_workflow=true` | asked | Write the starter workflow when the file doesn't exist. | +| `allow_unverified=true` | `false` | Go on when setup cannot check the workflow or the repository variable, or when the workflow has a step it cannot verify. Check the workflow yourself first: it is what keeps forks and PR code from the token. | + +##### What setup checks in the workflow + +The trust policies pin the workflow file at the default branch, `pull_request_target`, and an environment, so a PR cannot change what runs with the preview identity. Whatever that workflow does with the identity is up to the file, so setup reads it first. + +It looks only at the jobs that hold the token, those with `permissions: id-token: write`, and at reusable-workflow jobs. It refuses, whatever you pass: + +- a workflow that cannot match the trust policies: no `pull_request_target` trigger, a branch filter that leaves out the default branch, no job in the `preview` environment with `id-token: write`, or a job in `preview-cleanup` without it; +- a job that a fork's PR could run: one with no condition such as `github.event.pull_request.head.repo.full_name == github.repository` on it; +- a job that checks out or fetches the PR's head: `gh pr checkout`, a `git` checkout, switch, fetch, pull, worktree, clone or reset, or an `actions/checkout` `ref` or `repository`, that names the head on the line, through an `env` value, or through a `VAR=value` assignment in the job's scripts. + +It stops unless you pass `allow_unverified=true` for what it cannot decide: a same-repository condition in a form it doesn't recognize, or one inherited through `needs`; a `run` step with `git` fetch or checkout code that also takes any input (`$`, a backtick, `<`, `|` or `xargs`), even one that fetches only the base branch; an `actions/checkout` `ref` or `repository` given as an expression; and a reusable-workflow job. It also notes, without stopping, a workflow that never drops a closed PR's preview: one with no job in `preview-cleanup`, or whose `types` leave out `closed`. + +The check reads the workflow file only. It does not read scripts or local actions the workflow runs, and a head commit fetched without `git`, such as a downloaded tarball, is not caught. + +#### `harper preview teardown` + +```bash +harper preview teardown app=my-app +``` + +Calls [`disable_previews`](../operations-api/operations.md#disable_previews): it revokes the preview identity by dropping its trust policies and deactivating its user, drops every preview of the application on every node, then removes the user and the role, and prints what each node dropped. If a node fails or does not answer, it stops with the identity revoked; run it again to finish, which you have to before setup can enable previews again. It leaves the GitHub side as it is: the workflow file, the `HARPER_PREVIEW_TARGET` variable, and the GitHub environments. A credential setup sealed is left in the secrets store too; remove it with [`delete_secret`](../operations-api/operations.md#delete_secret). + +#### `harper preview deploy` + +Deploys or updates the preview of one PR. This is what the starter workflow runs, as the preview user: + +```bash +harper preview deploy app=my-app pr=42 branch=fix/cart by_ref=true ref= +``` + +`app`, `pr` and `branch` are required. Every argument other than the ones below goes to [`deploy_component`](../operations-api/operations.md#deploy_component) as it does for [`harper deploy`](#harper-deploy), such as `by_ref`, `ref`, `package`, `credentials` and `restart`. With neither `by_ref` nor `package`, it packages and uploads the current directory. With `by_ref=true` on `pull_request_target`, always pass `ref=` the PR's head commit: without it, `by_ref` deploys `GITHUB_SHA`, which for that event is the base branch's commit. + +| Parameter | Default | Description | +| ------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `app=` | required | The application. The preview deploys as `-pr-`. | +| `pr=` | required | The PR number. | +| `branch=` | required | The PR's head branch, which names the preview's host. | +| `domain=` | `app.` | The preview host's domain: the one setup used, or the cluster refuses the deploy. Required when the target is an IP address or `localhost`. | +| `label=