@harperfast/template-vue-ts-studio 1.13.0 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,7 +77,8 @@ See the concrete examples embedded in each rule subsection below (GraphQL schema
77
77
 
78
78
  ### 4. Infrastructure & Ops (MEDIUM)
79
79
 
80
- - `deploying-to-harper-fabric` — How to deploy a Harper application to the Harper Fabric cloud.
80
+ - `deploying-to-harper-fabric` — How to deploy a Harper application to Harper Fabric with the CLI, check that the release was certified, and roll back by activating a previous release's `deployment_id`.
81
+ - `deploying-from-ci` — How to deploy a Harper application from GitHub Actions with OIDC trusted publishing, so CI stores no Harper credential, including the one-command setup with `harper deploy setup=true provider=github-actions`.
81
82
  - `creating-a-fabric-account-and-cluster` — How to create a Harper Fabric account, organization, and cluster.
82
83
  - `creating-harper-apps` — How to initialize a new Harper application using the CLI.
83
84
  - `serving-web-content` — How to serve static files and integrated Vite/React applications in Harper.
@@ -16,7 +16,7 @@ Follow these steps to set up your Harper Fabric environment for deployment.
16
16
  3. **Create a Cluster**: Create a new cluster. This can be on the free tier, no credit card required.
17
17
  4. **Set Credentials**: During setup, set the cluster username and password to finish configuring it.
18
18
  5. **Get Application URL**: Navigate to the **Config** tab and copy the **Application URL**.
19
- 6. **Configure Environment**: Update your `.env` file or GitHub Actions secrets with cluster-specific credentials.
19
+ 6. **Connect the CLI**: Run `harper login` with the Application URL to store a token for the cluster, and set the URL as `HARPER_CLI_TARGET` in your `.env` file. Prefer this to putting the cluster username and password in `.env`, and don't store them in GitHub Actions secrets: to deploy from GitHub Actions, set up OIDC trusted publishing instead (see [deploying-from-ci](deploying-from-ci.md)).
20
20
  7. **Next Steps**: See the [deploying-to-harper-fabric](deploying-to-harper-fabric.md) rule for detailed instructions on deploying your application successfully.
21
21
 
22
22
  ## Examples
@@ -24,7 +24,10 @@ Follow these steps to set up your Harper Fabric environment for deployment.
24
24
  ### Environment Configuration
25
25
 
26
26
  ```bash
27
- CLI_TARGET_USERNAME='YOUR_CLUSTER_USERNAME'
28
- CLI_TARGET_PASSWORD='YOUR_CLUSTER_PASSWORD'
29
- CLI_TARGET='YOUR_CLUSTER_URL'
27
+ harper login YOUR_CLUSTER_URL
28
+ ```
29
+
30
+ ```bash
31
+ # .env
32
+ HARPER_CLI_TARGET='YOUR_CLUSTER_URL'
30
33
  ```
@@ -48,7 +48,74 @@ npm create harper@latest my-app --template default
48
48
 
49
49
  ## Next Steps
50
50
 
51
- 1. **Configure Environment**: Set up your `.env` file with local or cloud credentials.
52
- 2. **Define Schema**: Modify `schema.graphql` to fit your application's data model.
53
- 3. **Start Development**: Run `npm run dev` to start the local Harper instance.
54
- 4. **Deploy**: Use `npm run deploy` to push your application to Harper Fabric.
51
+ 1. **Define Schema**: Modify `schema.graphql` to fit your application's data model.
52
+ 2. **Start Development**: Run `npm run dev` to start the local Harper instance.
53
+ 3. **Connect the CLI to your cluster**: Create a cluster (see
54
+ [creating-a-fabric-account-and-cluster](creating-a-fabric-account-and-cluster.md)), run `harper login` with its
55
+ Application URL as a super user, and set that URL as `HARPER_CLI_TARGET` in the project's `.env` file. Don't put a
56
+ cluster username or password in `.env` or in GitHub secrets.
57
+ 4. **Deploy**: See [Deploying](#deploying).
58
+
59
+ ## Deploying
60
+
61
+ Every template except the legacy `early-hints` example ships a GitHub Actions workflow,
62
+ `.github/workflows/deploy.yaml`. It tests every pull request and deploys to the cluster when a change merges to `main`.
63
+ The deploy job holds no Harper credential: its `harper` commands authenticate with GitHub's OIDC identity token under a
64
+ trust policy on the cluster. The Next.js templates' workflow also runs `next build` before it uploads.
65
+
66
+ 1. **Set up CI deploys once**: Push the project to a GitHub repository, then run `npm run deploy:setup-ci` from the
67
+ project, logged in to the cluster as a super user. It runs `harper deploy setup=true provider=github-actions` for the
68
+ project, which creates a deploy-only role and user and a trust policy matched to the workflow, and sets the
69
+ repository's `HARPER_CLI_TARGET` variable when the `gh` CLI is signed in (otherwise it prints the command to run).
70
+ Running it again changes nothing. See [deploying-from-ci](deploying-from-ci.md) for what it checks and how to
71
+ customize it.
72
+
73
+ The command needs the Harper 5.4.0 CLI or later (`harper version`); an earlier CLI can't run it. Without 5.4.0, set
74
+ up the same role, user, policy and variable by hand, as a super user. The policy must match the scaffolded workflow:
75
+ its `deploy.yaml` file on `main`, in the `production` environment. The `audience` is the cluster URL with its port
76
+ and a trailing slash, and `HARPER_CLI_TARGET` must be that same string:
77
+
78
+ ```bash
79
+ gh api repos/<owner>/<repo> --jq .id # the repository_id
80
+ harper add_role role=<name>-ci-deploy permission='{"operations":["deploy_component","get_job","registration_info"]}'
81
+ harper add_user username=<name>-ci-deploy role=<name>-ci-deploy active=true password="$(openssl rand -base64 32)"
82
+ harper add_oidc_trust \
83
+ id=github-actions-<name> \
84
+ issuer=https://token.actions.githubusercontent.com \
85
+ audience=https://my-cluster.example.com:9925/ \
86
+ user=<name>-ci-deploy \
87
+ claims='{"repository_id":"<id>","workflow_ref":"<owner>/<repo>/.github/workflows/deploy.yaml@refs/heads/main","environment":"production"}'
88
+ gh variable set HARPER_CLI_TARGET --repo <owner>/<repo> --body https://my-cluster.example.com:9925/
89
+ ```
90
+
91
+ If you restrict the `production` environment's deployment branches, allow `main`.
92
+
93
+ 2. **Deploy by merging to `main`**, or by running the workflow from the **Actions** tab. The workflow deploys with
94
+ `restart=rolling` and waits for every node to take the release. The run's summary names the `deployment_id` and its
95
+ `certification`.
96
+ 3. **Deploy by hand with `npm run deploy`**: It runs `harper deploy project=<name> restart=rolling` from your machine, as
97
+ the user you logged in with.
98
+ 4. **Go back to an earlier release** by activating its `deployment_id`, with nothing reinstalled:
99
+
100
+ ```bash
101
+ harper list_deployments project=<name>
102
+ harper deploy project=<name> deployment_id=<id> restart=rolling
103
+ ```
104
+
105
+ See [deploying-to-harper-fabric](deploying-to-harper-fabric.md).
106
+
107
+ Use your package manager's equivalent of `npm run` (`pnpm run`, `bun run`, `yarn`) if the project uses another one.
108
+
109
+ Projects created before create-harper 1.13.0 deploy on version tags with a `HARPER_CLI_REFRESH_TOKEN` secret, and
110
+ don't have `deploy:setup-ci`. To move one over:
111
+
112
+ 1. Scaffold a new project with the same name and template, and copy its `.github/workflows/deploy.yaml` over the old
113
+ workflow.
114
+ 2. Set up the cluster for it, with the `project=` value that workflow passes to `harper deploy` as `<name>`: with the
115
+ 5.4.0 CLI, `harper deploy setup=true provider=github-actions project=<name>`; with an earlier one, by hand as in
116
+ step 1.
117
+ 3. Push the new workflow to `main`.
118
+ 4. Once the workflow's run on `main` has succeeded, wait step included, delete the `HARPER_CLI_REFRESH_TOKEN` secret.
119
+ Deleting it doesn't revoke the token, which stays valid until it expires. To cut it off now, mint a new one for that
120
+ user with `harper login --for-ci`, which invalidates the old one, or deactivate the user with `alter_user` if
121
+ nothing else uses it.
@@ -0,0 +1,358 @@
1
+ ---
2
+ name: deploying-from-ci
3
+ description: >-
4
+ How to deploy a Harper application from GitHub Actions with OIDC trusted
5
+ publishing, so CI stores no Harper credential, including the one-command setup
6
+ with `harper deploy setup=true provider=github-actions`.
7
+ metadata:
8
+ mode: generate
9
+ sources:
10
+ - reference/v5/cli/commands.md#GitHub Actions deploys (OIDC)
11
+ - >-
12
+ learn/developers/deploying-from-ci.md#Let the workflow authenticate with
13
+ OIDC
14
+ - >-
15
+ learn/developers/deploying-from-ci.md#Path A: deploy a tagged release from
16
+ your repository
17
+ - >-
18
+ learn/developers/deploying-from-ci.md#Give Harper a durable credential for
19
+ a private source
20
+ - >-
21
+ learn/developers/deploying-from-ci.md#Path B: build in CI, publish to
22
+ GitHub Packages, deploy the artifact
23
+ - learn/developers/deploying-from-ci.md#Check the deployment record
24
+ - learn/developers/deploying-from-ci.md#Operational notes
25
+ - reference/v5/cli/authentication.md#Workload identity (OIDC)
26
+ - >-
27
+ reference/v5/operations-api/operations.md#Policy specificity for GitHub
28
+ Actions
29
+ - reference/v5/operations-api/operations.md#`list_oidc_trust`
30
+ - reference/v5/operations-api/operations.md#`drop_oidc_trust`
31
+ sourceCommit: 2dde0597443a13af0c753f2adbe528e8442d8ee4
32
+ inputHash: b235eb6a7fb4d9a1
33
+ ---
34
+
35
+ # Deploying a Harper Application from GitHub Actions with OIDC
36
+
37
+ Instructions for the agent to configure and execute credential-free Harper deployments from GitHub Actions using OIDC trusted publishing, including one-command setup and complete workflow examples.
38
+
39
+ ## When to Use
40
+
41
+ Apply this rule when asked to deploy from CI, to set up or fix a deploy workflow (including the `.github/workflows/deploy.yaml` that create-harper projects ship), or to move a pipeline off a stored Harper credential such as `HARPER_CLI_REFRESH_TOKEN`. Use it when setting up OIDC trust for the first time, writing or debugging a deploy workflow, or revoking/rotating pipeline access.
42
+
43
+ ## How It Works
44
+
45
+ ### One-Command Setup (Branch-Based Deploys)
46
+
47
+ 1. **Run setup once** from the project checkout, signed in as a `super_user`:
48
+
49
+ ```bash
50
+ harper login https://my-cluster.example.com:9925
51
+ harper deploy setup=true provider=github-actions project=my-app
52
+ ```
53
+
54
+ If your workflow file is not `.github/workflows/deploy.yaml`, pass `workflow=` explicitly — otherwise the policy pins `deploy.yaml` and runs of your workflow cannot match it:
55
+
56
+ ```bash
57
+ harper deploy setup=true provider=github-actions project=my-app workflow=.github/workflows/deploy.yml
58
+ ```
59
+
60
+ 2. **Understand what setup does** — in order:
61
+ - Resolves the GitHub repository's numeric id and canonical name from the `origin` remote (or `repo=<owner>/<name>`). Pass `repository_id=<id>` when the API is unavailable (e.g., private repo without `gh`).
62
+ - **The workflow is checked before anything is written.** It must be able to run on `main` (`branch=`) and run a job in the `production` environment (`environment=`) with `permissions: id-token: write`; one that never could stops setup. What it can't decide is reported as not verified.
63
+ - Creates a role and a user named `<project>-ci-deploy` with permissions limited to `deploy_component`, `get_job`, and `registration_info`, and a trust policy named `github-actions-<project>`.
64
+ - Reads the policy back and compares it claim by claim with what it intended to write.
65
+ - Sets the repository's `HARPER_CLI_TARGET` Actions variable via `gh` if one does not already exist.
66
+
67
+ 3. **Handle idempotency**: setup only creates — it never updates. A role, user, or policy that already exists but differs stops setup and names what differs. To replace a policy:
68
+
69
+ ```bash
70
+ harper drop_oidc_trust id=github-actions-<project>
71
+ harper deploy setup=true provider=github-actions project=my-app
72
+ ```
73
+
74
+ ### Manual Setup (Tag-Based or Custom Deploys)
75
+
76
+ Use this path when deploying on version tags, because a branch-pinned policy cannot match a tag ref.
77
+
78
+ 1. **Create a deploy-only role and user**:
79
+
80
+ ```bash
81
+ harper add_role role=ci_deploy permission='{"operations":["deploy_component","get_job","get_deployment","registration_info"]}'
82
+ harper add_user username=ci-deploy role=ci_deploy active=true password="$(openssl rand -base64 32)"
83
+ ```
84
+
85
+ - `deploy_component` — the deploy itself.
86
+ - `get_job` — required for the wait step; an `operations` allowlist refuses anything not listed, so without it the wait step fails with `403`.
87
+ - `get_deployment` — reads the deployment record.
88
+ - `registration_info` — version check `harper deploy` makes before uploading.
89
+ - `restart_service` is not needed: Harper starts the rolling restart's job itself. The password is required to create the user but a GitHub Actions pipeline never uses it, so this command discards it.
90
+
91
+ 2. **Add a trust policy** pinned to `repository_id`, `workflow_path`, and `environment` (use this shape when the tag is unknown at policy-write time):
92
+
93
+ ```bash
94
+ harper add_oidc_trust \
95
+ id=my-app-release \
96
+ issuer=https://token.actions.githubusercontent.com \
97
+ audience=https://my-cluster.example.com:9925/ \
98
+ user=ci-deploy \
99
+ claims='{"repository_id":"67890","workflow_path":"my-org/my-app/.github/workflows/deploy.yml","environment":"production"}'
100
+ ```
101
+
102
+ Key claim rules:
103
+ - `repository_id` is immutable — survives renames. Look it up with `gh api repos/my-org/my-app --jq .id`.
104
+ - `workflow_path` pins the workflow file. Harper derives it from the token's `workflow_ref` by removing the `@<ref>` suffix, so it is `<owner>/<repo>/<path>`.
105
+ - `environment` is the ref gate. Without it, anyone who can push a branch can mint a token.
106
+ - `audience` must be the exact target URL with its port and a **trailing slash**.
107
+ - A workflow that deploys on a push to `main` can pin `workflow_ref` (`my-org/my-app/.github/workflows/deploy.yml@refs/heads/main`) instead, which gates the ref by itself.
108
+
109
+ 3. **Configure the GitHub environment**: in **Settings → Environments → production**, set **Deployment branches and tags** to the `v*` tag pattern. Add required reviewers if a release needs approval.
110
+
111
+ 4. **Store the target as a repository variable** (not a secret — it is not sensitive):
112
+
113
+ ```bash
114
+ gh variable set HARPER_CLI_TARGET --repo my-org/my-app --body https://my-cluster.example.com:9925/
115
+ ```
116
+
117
+ ### How OIDC Authentication Works in the Workflow
118
+
119
+ Once a trust policy exists, every `harper` command in the deploy job authenticates through it automatically when all four conditions hold:
120
+
121
+ 1. **The job may request an identity token.** On GitHub Actions that is `permissions: id-token: write` on the job.
122
+ 2. **`HARPER_CLI_TARGET` is the policy's `audience`.** The CLI requests its token for the target it connects to, so the target must be the URL the policy names, with its port and trailing slash. Use `vars.HARPER_CLI_TARGET` — it is not sensitive, so a repository variable rather than a secret.
123
+ 3. **The run matches the policy's claims**: its repository, its workflow file and branch, and its environment.
124
+ 4. **Nothing else is configured to authenticate.** `auth_username=`/`auth_password=`, credentials in the target URL, `HARPER_CLI_USERNAME`/`HARPER_CLI_PASSWORD`, `HARPER_CLI_REFRESH_TOKEN` or `HARPER_CLI_OPERATION_TOKEN`, and a saved `harper login` all outrank the exchange.
125
+
126
+ What each command may then do is up to the role of the user the policy names: a role that lists only `deploy_component`, `get_job` and `registration_info`, like the one setup creates, refuses anything else with a `403`.
127
+
128
+ The exchange runs only when GitHub sets `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, which it does for a job that declares `permissions: id-token: write`. Without them, the CLI falls through to its other credential sources rather than reporting a failure.
129
+
130
+ Nothing in the workflow names the policy. The CLI prints to stderr which policy authenticated it and as whom:
131
+
132
+ ```
133
+ Requesting a GitHub Actions identity token for https://my-instance.harperdb.io:9925/...
134
+ Authenticated as 'ci-deploy' via OIDC trust policy 'my-app-prod'.
135
+ ```
136
+
137
+ Diagnose a rejected exchange with `list_oidc_trust` (check for `invalid_reason` on each entry) and the instance's `oidc-trust` log. The server deliberately does not report which check failed.
138
+
139
+ ### Policy Specificity for GitHub Actions
140
+
141
+ For `https://token.actions.githubusercontent.com`, a policy must satisfy all three of these:
142
+
143
+ | Requirement | Satisfied by one of | Left open otherwise |
144
+ | ---------------------- | ------------------------------------------------------------------------ | ----------------------------------------------- |
145
+ | **Pin the repository** | `repository_id`, `repository` | Any repository |
146
+ | **Pin the workflow** | `workflow_ref`, `workflow_path`, `job_workflow_ref`, `job_workflow_path` | Any workflow in that repository |
147
+ | **Gate the ref** | `workflow_ref`, `ref`, `environment` | Any branch that can be pushed to the repository |
148
+
149
+ Additional constraints:
150
+
151
+ - `repository_id` is preferred over `repository` — it is immutable and survives renames.
152
+ - A tag-triggered release cannot pin `workflow_ref` since the tag is unknown when the policy is written. Pin `workflow_path` and gate on `environment`.
153
+ - `ref_type: tag` is not accepted as a ref gate, `sub` is not accepted as a pin, and `job_workflow_ref` pins the workflow without gating the ref. `pull_request_target` runs are denied unless the policy constrains `event_name`.
154
+
155
+ ### Revoking Access
156
+
157
+ - **Stop future runs**: `harper drop_oidc_trust id=github-actions-<project>` (does not revoke already-issued tokens, which are valid up to one hour).
158
+ - **Stop an already-issued token**: `harper alter_user username=<project>-ci-deploy active=false`.
159
+ - **No CI secret to rotate** — there is none.
160
+
161
+ ## Examples
162
+
163
+ ### Path A: Deploy a Tagged Release (`v1.2.3`) from the Repository
164
+
165
+ Push a semver tag (`v1.2.3`), and every node clones and installs that commit. `by_ref=true` deploys `git+https://github.com/<owner>/<repo>.git#<sha>`, where the SHA is the commit the tag points to (`GITHUB_SHA`), and every node clones that exact commit even if the tag moves later. For a private repository, add `credential=true` to attach the stored credential (`deploy.my-app.git.github.com`). If the repository needs a build step to be runnable, don't reach for `install_allow_scripts`: use Path B.
166
+
167
+ ```yaml
168
+ # .github/workflows/deploy.yml
169
+ name: Deploy to Harper
170
+ on:
171
+ push:
172
+ tags: ['v*']
173
+
174
+ jobs:
175
+ deploy:
176
+ runs-on: ubuntu-latest
177
+ environment: production
178
+ permissions:
179
+ contents: read
180
+ id-token: write
181
+ env:
182
+ HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }}
183
+ steps:
184
+ - uses: actions/checkout@v4
185
+ with:
186
+ persist-credentials: false
187
+ - uses: actions/setup-node@v4
188
+ with:
189
+ node-version: 22
190
+ - run: npm install --global harper@^5.3
191
+ - name: Deploy the tagged commit
192
+ run: harper deploy project=my-app by_ref=true restart=rolling json=true > deploy.json
193
+ - name: Wait for every node to take the release
194
+ run: |
195
+ JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
196
+ [ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
197
+ LAST="could not be read"
198
+ for attempt in $(seq 60); do
199
+ sleep 10
200
+ harper get_job id="$JOB_ID" json=true > job.json || continue
201
+ case $(jq -r '.[0].status // "MISSING"' job.json) in
202
+ COMPLETE) exit 0 ;;
203
+ ERROR) jq -r '.[0].message' job.json; exit 1 ;;
204
+ MISSING) LAST="was not on the node that answered; its outcome is unknown" ;;
205
+ *) LAST="was still running" ;;
206
+ esac
207
+ done
208
+ echo "After 60 polls, job $JOB_ID $LAST"
209
+ exit 1
210
+ ```
211
+
212
+ Key details:
213
+
214
+ - **No secrets.** `HARPER_CLI_TARGET` is the only thing the workflow is given. `id-token: write` lets the CLI request an identity token, and `environment: production` puts the `environment` claim the trust policy requires into it.
215
+ - **`persist-credentials: false`** keeps the job's `GITHUB_TOKEN` out of `.git/config`.
216
+ - **`harper@^5.3`** pins the CLI's major version. 5.3 is the first CLI that authenticates with the identity token.
217
+ - **`replicated` isn't needed.** On Harper Fabric and Harper Pro, a deploy goes to every node unless you pass `replicated=false`. Each node fetches and installs the package itself.
218
+ - **`restart=rolling`** restarts nodes one at a time. Use `restart=true` for a single-node or dev instance.
219
+ - **`json=true`** prints the deploy's result as JSON on stdout; progress and authentication messages go to stderr.
220
+ - **The wait step** polls `restartJobId` via `get_job` until `COMPLETE` or `ERROR`, checking `.[0].status`. It retries a poll that fails (such as one that reaches a node while it restarts) and gives up after 60 polls (~10 minutes).
221
+ - **`certification`**: on v5.4.0 and later, `restart=rolling` loads the release in a canary worker on each node before that node serves it, so a release that fails to load is rejected. The `certification` field in the deploy result reflects this. With `restart=rolling` on v5.4.0 and later, the deploy can report `success` before the rolling job has visited all nodes — wait on the job, not the record.
222
+ - **The job lives on one node.** Through a **load-balanced** cluster URL, polls must eventually reach that node. Routing that always sends polls to a different node than the deploy causes the wait step to fail saying the job was not on the node that answered.
223
+
224
+ ### Path B: Build in CI, Publish to GitHub Packages, Deploy the Artifact
225
+
226
+ Give `package.json` a scoped name and `"publishConfig": { "registry": "https://npm.pkg.github.com" }`:
227
+
228
+ ```json
229
+ {
230
+ "name": "@my-org/my-app",
231
+ "version": "1.2.3",
232
+ "publishConfig": { "registry": "https://npm.pkg.github.com" },
233
+ "files": ["config.yaml", "schema.graphql", "dist/"]
234
+ }
235
+ ```
236
+
237
+ The deploy installs the version the tag names, and `npm publish` publishes the version in `package.json`, so they must agree: `npm version 1.2.3` sets one and creates the other as `v1.2.3`.
238
+
239
+ ```yaml
240
+ # .github/workflows/deploy.yml
241
+ name: Build, publish, and deploy to Harper
242
+ on:
243
+ push:
244
+ tags: ['v*']
245
+
246
+ jobs:
247
+ publish:
248
+ runs-on: ubuntu-latest
249
+ permissions:
250
+ contents: read
251
+ packages: write
252
+ steps:
253
+ - uses: actions/checkout@v4
254
+ with:
255
+ persist-credentials: false
256
+ - uses: actions/setup-node@v4
257
+ with:
258
+ node-version: 22
259
+ registry-url: https://npm.pkg.github.com
260
+ scope: '@my-org'
261
+ - run: npm ci
262
+ env:
263
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
264
+ - run: npm run build
265
+ - run: npm test
266
+ - name: Publish to GitHub Packages
267
+ run: npm publish
268
+ env:
269
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
270
+
271
+ deploy:
272
+ needs: publish
273
+ runs-on: ubuntu-latest
274
+ environment: production
275
+ permissions:
276
+ id-token: write
277
+ env:
278
+ HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }}
279
+ steps:
280
+ - uses: actions/setup-node@v4
281
+ with:
282
+ node-version: 22
283
+ - run: npm install --global harper@^5.3
284
+ - name: Deploy the published version
285
+ run: |
286
+ harper deploy project=my-app package="@my-org/my-app@${GITHUB_REF_NAME#v}" \
287
+ credentials='[{"registry":"https://npm.pkg.github.com","scope":"@my-org","secret":"deploy.my-app.npm.pkg.github.com"}]' \
288
+ restart=rolling json=true > deploy.json
289
+ - name: Wait for every node to take the release
290
+ run: |
291
+ JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
292
+ [ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
293
+ LAST="could not be read"
294
+ for attempt in $(seq 60); do
295
+ sleep 10
296
+ harper get_job id="$JOB_ID" json=true > job.json || continue
297
+ case $(jq -r '.[0].status // "MISSING"' job.json) in
298
+ COMPLETE) exit 0 ;;
299
+ ERROR) jq -r '.[0].message' job.json; exit 1 ;;
300
+ MISSING) LAST="was not on the node that answered; its outcome is unknown" ;;
301
+ *) LAST="was still running" ;;
302
+ esac
303
+ done
304
+ echo "After 60 polls, job $JOB_ID $LAST"
305
+ exit 1
306
+ ```
307
+
308
+ Two jobs keep the identity token isolated: `publish` installs, builds, tests and publishes with `contents: read` and `packages: write` only — it cannot request an identity token. `deploy` runs once `publish` finishes (`needs: publish`), holds `environment: production` and `id-token: write`, and installs only the Harper CLI before deploying.
309
+
310
+ Credential roles:
311
+
312
+ - **Publishing** and installing private `@my-org` dependencies use the workflow's ephemeral `GITHUB_TOKEN` (`packages: write` includes read). Both happen entirely inside `publish`.
313
+ - **Installing on Harper nodes** uses the durable `read:packages` token stored as `deploy.my-app.npm.pkg.github.com` — because installs can happen long after this workflow ends.
314
+
315
+ For `my-app`, the credential names are `deploy.my-app.git.github.com` for the repository and `deploy.my-app.npm.pkg.github.com` for GitHub Packages. Store a durable credential once from your machine:
316
+
317
+ ```bash
318
+ harper deploy setup=true provider=github project=my-app
319
+ # or for GitHub Packages:
320
+ harper deploy setup=true provider=npm project=my-app registry=https://npm.pkg.github.com scope=@my-org
321
+ ```
322
+
323
+ For a private repository, use a GitHub fine-grained personal access token with **Contents: read-only** scoped to the application repository. For GitHub Packages, use a token with the `read:packages` scope. Do not use the workflow's ephemeral `GITHUB_TOKEN` for the durable credential — it expires when the job ends.
324
+
325
+ ### Check the Deployment Record
326
+
327
+ ```bash
328
+ harper get_deployment deployment_id="$(jq -r .deployment_id deploy.json)" json=true
329
+ ```
330
+
331
+ Only the node you called writes that record, so with `restart=rolling` on v5.4.0 and later it can report `success` before the job has visited the other nodes, and keeps reporting it if one of them rejects the release. Wait on the job, not the record, to confirm all nodes took the release. `list_deployments` gives the history of what was deployed and when.
332
+
333
+ ### List and Inspect Trust Policies
334
+
335
+ ```bash
336
+ harper list_oidc_trust
337
+ ```
338
+
339
+ Returns `{ "policies": [ ... ] }`. Each entry includes `id`, `issuer`, `audience`, `claims`, `user`, `enabled`, and optionally `invalid_reason`. Check `invalid_reason` first when diagnosing a rejected exchange — it is set when the policy is malformed or its user was deleted or deactivated.
340
+
341
+ ### Drop a Trust Policy
342
+
343
+ ```bash
344
+ harper drop_oidc_trust id=github-actions-<project>
345
+ ```
346
+
347
+ ## Notes
348
+
349
+ - `HARPER_CLI_REFRESH_TOKEN` and any other stored credential outrank the OIDC exchange. Remove them from the workflow if switching to OIDC.
350
+ - **Go back to a previous release** by activating its `deployment_id`, with no rebuild or reinstall: `harper deploy project=my-app deployment_id=<id> restart=rolling`. To deploy a release that is no longer kept, re-run the deploy with its tag or version.
351
+ - **Rotating the deploy credential**: run `harper deploy setup=true` again with the same name and a new value. The pipeline doesn't change.
352
+ - **Revoking the pipeline's access**: `harper drop_oidc_trust` stops new runs; `harper alter_user` also stops a token already issued (valid up to one hour).
353
+ - The `<project>-ci-deploy` user created by setup can deploy any component on the cluster, not only the named project. Access control comes down to who can merge to the branch.
354
+ - The `audience` in `add_oidc_trust` must be the exact target URL with its port and a trailing slash.
355
+ - For tag-triggered workflows, `provider=github-actions` setup is not sufficient — it pins a branch. Write the policy manually using `add_oidc_trust` with `workflow_path` and `environment` claims.
356
+ - `token.actions.githubusercontent.com` is the required `issuer` value for GitHub Actions OIDC policies.
357
+ - Git-host credentials require git ≥ 2.31 on the Harper nodes and are not supported on Windows nodes. Registry credentials have no such constraints.
358
+ - See [deploying-to-harper-fabric.md](deploying-to-harper-fabric.md) for cluster-level deployment context.