@harperfast/template-vue-ts-studio 1.12.12 → 1.13.1

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.
Files changed (23) hide show
  1. package/.agents/skills/harper-best-practices/AGENTS.md +684 -239
  2. package/.agents/skills/harper-best-practices/SKILL.md +2 -1
  3. package/.agents/skills/harper-best-practices/rules/creating-a-fabric-account-and-cluster.md +7 -4
  4. package/.agents/skills/harper-best-practices/rules/creating-harper-apps.md +71 -4
  5. package/.agents/skills/harper-best-practices/rules/custom-resources.md +63 -29
  6. package/.agents/skills/harper-best-practices/rules/deploying-from-ci.md +358 -0
  7. package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +132 -103
  8. package/.agents/skills/harper-best-practices/rules/load-env.md +37 -49
  9. package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +71 -57
  10. package/.agents/skills/harper-best-practices/rules.manifest.yaml +184 -8
  11. package/README.md +0 -48
  12. package/agent/skills/harper-best-practices/AGENTS.md +684 -239
  13. package/agent/skills/harper-best-practices/SKILL.md +2 -1
  14. package/agent/skills/harper-best-practices/rules/creating-a-fabric-account-and-cluster.md +7 -4
  15. package/agent/skills/harper-best-practices/rules/creating-harper-apps.md +71 -4
  16. package/agent/skills/harper-best-practices/rules/custom-resources.md +63 -29
  17. package/agent/skills/harper-best-practices/rules/deploying-from-ci.md +358 -0
  18. package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +132 -103
  19. package/agent/skills/harper-best-practices/rules/load-env.md +37 -49
  20. package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +71 -57
  21. package/agent/skills/harper-best-practices/rules.manifest.yaml +184 -8
  22. package/package.json +1 -1
  23. package/skills-lock.json +1 -1
@@ -76,7 +76,8 @@ See the concrete examples embedded in each rule subsection below (GraphQL schema
76
76
 
77
77
  ### 4. Infrastructure & Ops (MEDIUM)
78
78
 
79
- - `deploying-to-harper-fabric` — How to deploy a Harper application to the Harper Fabric cloud.
79
+ - `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`.
80
+ - `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`.
80
81
  - `creating-a-fabric-account-and-cluster` — How to create a Harper Fabric account, organization, and cluster.
81
82
  - `creating-harper-apps` — How to initialize a new Harper application using the CLI.
82
83
  - `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.
@@ -7,8 +7,8 @@ metadata:
7
7
  - reference/v5/resources/overview.md#Custom External Data Source
8
8
  - reference/v5/resources/overview.md#Exporting Resources as Endpoints
9
9
  - reference/v5/components/javascript-environment.md#Module Formats
10
- sourceCommit: 9e6ecf87dd25bb488ad44dc2876ca6439ccd682e
11
- inputHash: 86858206e33925f0
10
+ sourceCommit: e73e5efb2823cc52caf7a6458d67fbcff3bbff2c
11
+ inputHash: b2f8749a92d64168
12
12
  ---
13
13
 
14
14
  # Custom Resources
@@ -17,7 +17,7 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
17
17
 
18
18
  ## When to Use
19
19
 
20
- Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or registering routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed resource or when a specific URL shape is required.
20
+ Apply this rule when you need to create custom HTTP endpoints, wrap external APIs, or register routes programmatically in a Harper application. Use it any time business logic must live outside a table-backed schema, or when you need fine-grained control over URL shape and path parameters.
21
21
 
22
22
  ## How It Works
23
23
 
@@ -27,7 +27,13 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
27
27
  import { tables, Resource } from 'harper';
28
28
  ```
29
29
 
30
- 2. **Define a class that `extends Resource`**: Implement HTTP methods as `static` methods. Each method receives a `target` object.
30
+ CommonJS alternative:
31
+
32
+ ```javascript
33
+ const { tables, Resource } = require('harper');
34
+ ```
35
+
36
+ 2. **Define a class that `extends Resource`**: Use `export class` so Harper exposes it as an endpoint. Implement HTTP methods as `static` methods on the class.
31
37
 
32
38
  ```javascript
33
39
  export class CustomEndpoint extends Resource {
@@ -39,7 +45,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
39
45
  }
40
46
  ```
41
47
 
42
- 3. **Use `async` static methods to call external services**: Return or forward the response directly.
48
+ 3. **Use `static` async methods to call external services**: Each HTTP verb maps to a `static` method of the same lowercase name.
43
49
 
44
50
  ```javascript
45
51
  export class MyExternalData extends Resource {
@@ -57,24 +63,20 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
57
63
  }
58
64
  ```
59
65
 
60
- 4. **Export the class to expose it as an endpoint**: The export form controls the resulting URL. Choose the form that matches the URL shape you need.
66
+ 4. **Control the URL by choosing the export form**: The shape of the export determines the resulting URL path.
61
67
 
62
68
  | Export form | URL | Notes |
63
69
  | ------------------------------------------- | --------------- | --------------------------------------------------------------- |
64
- | `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment. Case-sensitive. |
70
+ | `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment; case-sensitive. |
65
71
  | `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
66
72
  | `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
67
73
  | `export { Foo as '/widget/:id' }` | `/widget/:id` | Rename the export to set the path directly. |
68
74
  | `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
69
- | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration for dynamic paths. |
75
+ | `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration; useful when the path is dynamic. |
70
76
 
71
- 5. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
72
-
73
- ```javascript
74
- server.resources.set('my-path', Foo);
75
- ```
77
+ URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
76
78
 
77
- 6. **Declare dynamic path segments with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
79
+ 5. **Declare dynamic path segments with `static path`**: Use `:name` for a single segment and `*name` as a catch-all. Matched values are bound onto `target.<name>`.
78
80
 
79
81
  ```javascript
80
82
  export class Widget extends Resource {
@@ -84,13 +86,36 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
84
86
  return { id: target.id, action: target.action };
85
87
  }
86
88
  }
89
+
90
+ export class Files extends Resource {
91
+ // GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
92
+ static path = '/files/*rest';
93
+ static get(target) {
94
+ return { path: target.rest };
95
+ }
96
+ }
87
97
  ```
88
98
 
89
- 7. **Resolve path precedence correctly**: Exact and static paths always win over parameterized ones. Among parameterized routes, more specific paths win: a literal segment beats a `:param`, which beats a `*` wildcard, compared left to right.
99
+ - A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
100
+ - A leading `./` or bare name resolves relative to the component directory.
101
+ - A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment.
102
+ - `static path` takes precedence over the export name.
103
+ - Exact and static paths always win over parameterized ones. Among parameterized routes, more specific paths win: a literal segment beats `:param`, which beats `*`, compared left to right.
104
+
105
+ 6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
106
+
107
+ ```javascript
108
+ server.resources.set('my-path', Foo);
109
+ ```
110
+
111
+ 7. **Optionally use a resource as a cache source for a local table**: Call `sourcedFrom` on the target table.
112
+ ```javascript
113
+ tables.MyCache.sourcedFrom(MyExternalData);
114
+ ```
90
115
 
91
116
  ## Examples
92
117
 
93
- **Wrapping an external API and using it as a cache source:**
118
+ **Custom endpoint with external API wrapping:**
94
119
 
95
120
  ```javascript
96
121
  import { tables, Resource } from 'harper';
@@ -113,9 +138,19 @@ export class MyExternalData extends Resource {
113
138
  tables.MyCache.sourcedFrom(MyExternalData);
114
139
  ```
115
140
 
116
- **Catch-all wildcard path:**
141
+ **Path parameters with `static path`:**
117
142
 
118
143
  ```javascript
144
+ import { Resource } from 'harper';
145
+
146
+ export class Widget extends Resource {
147
+ // GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
148
+ static path = '/widget/:id/action/:action';
149
+ static get(target) {
150
+ return { id: target.id, action: target.action };
151
+ }
152
+ }
153
+
119
154
  export class Files extends Resource {
120
155
  // GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
121
156
  static path = '/files/*rest';
@@ -125,24 +160,23 @@ export class Files extends Resource {
125
160
  }
126
161
  ```
127
162
 
128
- **Root-relative fixed route:**
163
+ **Programmatic registration:**
129
164
 
130
165
  ```javascript
131
- export class AcmeChallenge extends Resource {
132
- static path = '/.well-known/acme-challenge/:token';
166
+ import { Resource } from 'harper';
167
+
168
+ export class Foo extends Resource {
133
169
  static get(target) {
134
- return { token: target.token };
170
+ return { ok: true };
135
171
  }
136
172
  }
173
+
174
+ server.resources.set('my-path', Foo);
137
175
  ```
138
176
 
139
177
  ## Notes
140
178
 
141
- - URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
142
- - A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
143
- - A leading `./` or a bare name in `static path` resolves relative to the component directory.
144
- - A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment of the path.
145
- - `static path` takes precedence over the export name when both are present.
146
- - Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as URI templates.
147
- - When a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
148
- - Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
179
+ - Avoid conflicting exports between the schema and the JavaScript implementation when a resource `extends` an existing table.
180
+ - Parameterized routes appear in the generated OpenAPI document as templated paths (e.g. `/widget/{id}/action/{action}`) and in MCP `resources/templates/list` as `{param}` URI templates.
181
+ - When bundling for SSR with a tool like Vite, keep `harper` external so it resolves to the runtime: `ssr: { external: ['harper'] }`.
182
+ - For components in their own directory, run `npm link harper` to ensure typings match the running installation.
@@ -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.