@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.
- package/.agents/skills/harper-best-practices/AGENTS.md +684 -239
- package/.agents/skills/harper-best-practices/SKILL.md +2 -1
- package/.agents/skills/harper-best-practices/rules/creating-a-fabric-account-and-cluster.md +7 -4
- package/.agents/skills/harper-best-practices/rules/creating-harper-apps.md +71 -4
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +63 -29
- package/.agents/skills/harper-best-practices/rules/deploying-from-ci.md +358 -0
- package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +132 -103
- package/.agents/skills/harper-best-practices/rules/load-env.md +37 -49
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +71 -57
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +184 -8
- package/README.md +0 -48
- package/agent/skills/harper-best-practices/AGENTS.md +684 -239
- package/agent/skills/harper-best-practices/SKILL.md +2 -1
- package/agent/skills/harper-best-practices/rules/creating-a-fabric-account-and-cluster.md +7 -4
- package/agent/skills/harper-best-practices/rules/creating-harper-apps.md +71 -4
- package/agent/skills/harper-best-practices/rules/custom-resources.md +63 -29
- package/agent/skills/harper-best-practices/rules/deploying-from-ci.md +358 -0
- package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +132 -103
- package/agent/skills/harper-best-practices/rules/load-env.md +37 -49
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +71 -57
- package/agent/skills/harper-best-practices/rules.manifest.yaml +184 -8
- package/package.json +1 -1
- package/skills-lock.json +1 -1
|
@@ -1605,7 +1605,7 @@ Instructions for the agent to follow when defining custom REST endpoints with Ja
|
|
|
1605
1605
|
|
|
1606
1606
|
#### When to Use
|
|
1607
1607
|
|
|
1608
|
-
Apply this rule when
|
|
1608
|
+
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.
|
|
1609
1609
|
|
|
1610
1610
|
#### How It Works
|
|
1611
1611
|
|
|
@@ -1615,7 +1615,13 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1615
1615
|
import { tables, Resource } from 'harper';
|
|
1616
1616
|
```
|
|
1617
1617
|
|
|
1618
|
-
|
|
1618
|
+
CommonJS alternative:
|
|
1619
|
+
|
|
1620
|
+
```javascript
|
|
1621
|
+
const { tables, Resource } = require('harper');
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
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.
|
|
1619
1625
|
|
|
1620
1626
|
```javascript
|
|
1621
1627
|
export class CustomEndpoint extends Resource {
|
|
@@ -1627,7 +1633,7 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1627
1633
|
}
|
|
1628
1634
|
```
|
|
1629
1635
|
|
|
1630
|
-
3. **Use `
|
|
1636
|
+
3. **Use `static` async methods to call external services**: Each HTTP verb maps to a `static` method of the same lowercase name.
|
|
1631
1637
|
|
|
1632
1638
|
```javascript
|
|
1633
1639
|
export class MyExternalData extends Resource {
|
|
@@ -1645,24 +1651,20 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1645
1651
|
}
|
|
1646
1652
|
```
|
|
1647
1653
|
|
|
1648
|
-
4. **
|
|
1654
|
+
4. **Control the URL by choosing the export form**: The shape of the export determines the resulting URL path.
|
|
1649
1655
|
|
|
1650
1656
|
| Export form | URL | Notes |
|
|
1651
1657
|
| ------------------------------------------- | --------------- | --------------------------------------------------------------- |
|
|
1652
|
-
| `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment
|
|
1658
|
+
| `export class Foo extends Resource {}` | `/Foo/` | Class name becomes the path segment; case-sensitive. |
|
|
1653
1659
|
| `export const Bar = { Foo };` | `/Bar/Foo/` | Nest under an object to add a path prefix. |
|
|
1654
1660
|
| `export const bar = { 'foo-baz': Foo };` | `/bar/foo-baz/` | Use object keys for lowercase, hyphens, or non-identifier URLs. |
|
|
1655
1661
|
| `export { Foo as '/widget/:id' }` | `/widget/:id` | Rename the export to set the path directly. |
|
|
1656
1662
|
| `static path = '/widget/:id'` (class field) | `/widget/:id` | Declare path on the class; overrides the export name. |
|
|
1657
|
-
| `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration
|
|
1663
|
+
| `server.resources.set('my-path', Foo);` | `/my-path/` | Programmatic registration; useful when the path is dynamic. |
|
|
1658
1664
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
```javascript
|
|
1662
|
-
server.resources.set('my-path', Foo);
|
|
1663
|
-
```
|
|
1665
|
+
URL path matching is case-sensitive — `/Foo/` and `/foo/` are different endpoints.
|
|
1664
1666
|
|
|
1665
|
-
|
|
1667
|
+
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>`.
|
|
1666
1668
|
|
|
1667
1669
|
```javascript
|
|
1668
1670
|
export class Widget extends Resource {
|
|
@@ -1672,13 +1674,36 @@ Apply this rule when creating custom HTTP endpoints, wrapping external APIs, or
|
|
|
1672
1674
|
return { id: target.id, action: target.action };
|
|
1673
1675
|
}
|
|
1674
1676
|
}
|
|
1677
|
+
|
|
1678
|
+
export class Files extends Resource {
|
|
1679
|
+
// GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
|
|
1680
|
+
static path = '/files/*rest';
|
|
1681
|
+
static get(target) {
|
|
1682
|
+
return { path: target.rest };
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1675
1685
|
```
|
|
1676
1686
|
|
|
1677
|
-
|
|
1687
|
+
- A leading `/` in `static path` makes the path root-relative (top-level), independent of the file's location.
|
|
1688
|
+
- A leading `./` or bare name resolves relative to the component directory.
|
|
1689
|
+
- A bare `*` (no name) binds under `target.wildcard`. A wildcard must be the final segment.
|
|
1690
|
+
- `static path` takes precedence over the export name.
|
|
1691
|
+
- 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.
|
|
1692
|
+
|
|
1693
|
+
6. **Register programmatically when the path is dynamic**: Use `server.resources.set(` with a path string and the resource class.
|
|
1694
|
+
|
|
1695
|
+
```javascript
|
|
1696
|
+
server.resources.set('my-path', Foo);
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
7. **Optionally use a resource as a cache source for a local table**: Call `sourcedFrom` on the target table.
|
|
1700
|
+
```javascript
|
|
1701
|
+
tables.MyCache.sourcedFrom(MyExternalData);
|
|
1702
|
+
```
|
|
1678
1703
|
|
|
1679
1704
|
#### Examples
|
|
1680
1705
|
|
|
1681
|
-
**
|
|
1706
|
+
**Custom endpoint with external API wrapping:**
|
|
1682
1707
|
|
|
1683
1708
|
```javascript
|
|
1684
1709
|
import { tables, Resource } from 'harper';
|
|
@@ -1701,9 +1726,19 @@ export class MyExternalData extends Resource {
|
|
|
1701
1726
|
tables.MyCache.sourcedFrom(MyExternalData);
|
|
1702
1727
|
```
|
|
1703
1728
|
|
|
1704
|
-
**
|
|
1729
|
+
**Path parameters with `static path`:**
|
|
1705
1730
|
|
|
1706
1731
|
```javascript
|
|
1732
|
+
import { Resource } from 'harper';
|
|
1733
|
+
|
|
1734
|
+
export class Widget extends Resource {
|
|
1735
|
+
// GET /widget/10/action/jump -> target.id === '10', target.action === 'jump'
|
|
1736
|
+
static path = '/widget/:id/action/:action';
|
|
1737
|
+
static get(target) {
|
|
1738
|
+
return { id: target.id, action: target.action };
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1707
1742
|
export class Files extends Resource {
|
|
1708
1743
|
// GET /files/a/b/c.txt -> target.rest === 'a/b/c.txt'
|
|
1709
1744
|
static path = '/files/*rest';
|
|
@@ -1713,27 +1748,26 @@ export class Files extends Resource {
|
|
|
1713
1748
|
}
|
|
1714
1749
|
```
|
|
1715
1750
|
|
|
1716
|
-
**
|
|
1751
|
+
**Programmatic registration:**
|
|
1717
1752
|
|
|
1718
1753
|
```javascript
|
|
1719
|
-
|
|
1720
|
-
|
|
1754
|
+
import { Resource } from 'harper';
|
|
1755
|
+
|
|
1756
|
+
export class Foo extends Resource {
|
|
1721
1757
|
static get(target) {
|
|
1722
|
-
return {
|
|
1758
|
+
return { ok: true };
|
|
1723
1759
|
}
|
|
1724
1760
|
}
|
|
1761
|
+
|
|
1762
|
+
server.resources.set('my-path', Foo);
|
|
1725
1763
|
```
|
|
1726
1764
|
|
|
1727
1765
|
#### Notes
|
|
1728
1766
|
|
|
1729
|
-
-
|
|
1730
|
-
-
|
|
1731
|
-
-
|
|
1732
|
-
-
|
|
1733
|
-
- `static path` takes precedence over the export name when both are present.
|
|
1734
|
-
- 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.
|
|
1735
|
-
- When a resource `extends` an existing table, avoid conflicting exports between the schema and the JavaScript implementation.
|
|
1736
|
-
- Link the `harper` package in your component directory to ensure correct typings: `npm link harper`. All installed components have `harper` automatically linked.
|
|
1767
|
+
- Avoid conflicting exports between the schema and the JavaScript implementation when a resource `extends` an existing table.
|
|
1768
|
+
- 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.
|
|
1769
|
+
- When bundling for SSR with a tool like Vite, keep `harper` external so it resolves to the runtime: `ssr: { external: ['harper'] }`.
|
|
1770
|
+
- For components in their own directory, run `npm link harper` to ensure typings match the running installation.
|
|
1737
1771
|
|
|
1738
1772
|
### 3.2 Extending Tables
|
|
1739
1773
|
|
|
@@ -1833,25 +1867,31 @@ export class MyTable extends tables.MyTable {
|
|
|
1833
1867
|
|
|
1834
1868
|
### 3.3 Programmatic Table Requests
|
|
1835
1869
|
|
|
1836
|
-
Instructions for the agent to
|
|
1870
|
+
Instructions for the agent to follow when interacting with Harper tables programmatically using the `tables` object.
|
|
1837
1871
|
|
|
1838
1872
|
#### When to Use
|
|
1839
1873
|
|
|
1840
|
-
Apply this rule when writing server-side Harper component code that reads from or writes to tables
|
|
1874
|
+
Apply this rule when writing server-side Harper component code that reads from or writes to tables using the programmatic API — for example, in HTTP handlers, background jobs, timers, or SSR entry points. Use it whenever you need to construct queries with `conditions`, `sort`, `select`, pagination, or transactions outside of the REST layer.
|
|
1841
1875
|
|
|
1842
1876
|
#### How It Works
|
|
1843
1877
|
|
|
1844
|
-
1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a
|
|
1878
|
+
1. **Import `tables` (and other APIs) from `harper`**: Access every table defined in `schema.graphql` as a property of `tables`. Each property is the table class implementing the Resource API.
|
|
1845
1879
|
|
|
1846
1880
|
```javascript
|
|
1847
1881
|
import { tables, transaction } from 'harper';
|
|
1848
1882
|
const { Product } = tables;
|
|
1849
|
-
//
|
|
1883
|
+
// same as: databases.data.Product
|
|
1884
|
+
```
|
|
1885
|
+
|
|
1886
|
+
For components in their own directory, run:
|
|
1887
|
+
|
|
1888
|
+
```bash
|
|
1889
|
+
npm link harper
|
|
1850
1890
|
```
|
|
1851
1891
|
|
|
1852
|
-
|
|
1892
|
+
All installed components have `harper` automatically linked.
|
|
1853
1893
|
|
|
1854
|
-
2. **Define your schema with `@table`**:
|
|
1894
|
+
2. **Define your schema with `@table`**: Every type you want to access via `tables` must carry the `@table` directive. Mark queryable fields with `@indexed`.
|
|
1855
1895
|
|
|
1856
1896
|
```graphql
|
|
1857
1897
|
type Product @table {
|
|
@@ -1861,7 +1901,7 @@ Apply this rule when writing server-side Harper component code that reads from o
|
|
|
1861
1901
|
}
|
|
1862
1902
|
```
|
|
1863
1903
|
|
|
1864
|
-
3. **Query records
|
|
1904
|
+
3. **Query records using `search(`**: Pass a Query object to `search()`. The query is an async iterable.
|
|
1865
1905
|
|
|
1866
1906
|
```javascript
|
|
1867
1907
|
const query = {
|
|
@@ -1872,7 +1912,7 @@ Apply this rule when writing server-side Harper component code that reads from o
|
|
|
1872
1912
|
}
|
|
1873
1913
|
```
|
|
1874
1914
|
|
|
1875
|
-
4. **Build `conditions`**: Each condition object supports
|
|
1915
|
+
4. **Build `conditions`**: Each condition object supports the following properties:
|
|
1876
1916
|
|
|
1877
1917
|
| Property | Description |
|
|
1878
1918
|
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -1882,53 +1922,60 @@ Apply this rule when writing server-side Harper component code that reads from o
|
|
|
1882
1922
|
| `conditions` | Nested conditions array |
|
|
1883
1923
|
| `operator` | `and` (default) or `or` for the nested `conditions` |
|
|
1884
1924
|
|
|
1885
|
-
5. **
|
|
1925
|
+
5. **Apply `select` to shape results**: Pass an array of property names, a string for a single property, or nested objects for relationships.
|
|
1886
1926
|
|
|
1887
1927
|
```javascript
|
|
1888
|
-
//
|
|
1928
|
+
// Flat select
|
|
1889
1929
|
Product.search({ select: ['name', 'price'] });
|
|
1890
1930
|
|
|
1891
|
-
//
|
|
1931
|
+
// Nested relationship select
|
|
1892
1932
|
Book.get({ id: 42, select: ['id', 'title', { name: 'author', select: ['name'] }] });
|
|
1893
1933
|
```
|
|
1894
1934
|
|
|
1895
1935
|
Special `select` values: `$id`, `$updatedtime`, `$distance`.
|
|
1896
1936
|
|
|
1897
|
-
6. **
|
|
1898
|
-
|
|
1899
|
-
```javascript
|
|
1900
|
-
Product.search({ conditions: [...], limit: 20, offset: 40 });
|
|
1901
|
-
```
|
|
1937
|
+
6. **Apply `sort`**: Harper uses an index to provide sort order.
|
|
1902
1938
|
|
|
1903
|
-
|
|
1939
|
+
| Property | Description |
|
|
1940
|
+
| ------------ | ---------------------------------------------------------- |
|
|
1941
|
+
| `attribute` | Property name (or array for chained relationship property) |
|
|
1942
|
+
| `descending` | Sort descending if `true` (default: `false`) |
|
|
1943
|
+
| `next` | Secondary sort to resolve ties (same structure) |
|
|
1944
|
+
- If the sort `attribute` is `@indexed`, no condition is required.
|
|
1945
|
+
- If the sort `attribute` is not indexed, at least one entry in `conditions` (on **any** attribute) is required.
|
|
1946
|
+
- Sorting by a non-indexed attribute with zero conditions raises:
|
|
1947
|
+
> `HdbError: <attribute> is not indexed and not combined with any other conditions`
|
|
1948
|
+
- The bare `@primaryKey` is treated as not indexed for sort purposes. To iterate in primary-key order, add an open-ended condition:
|
|
1904
1949
|
|
|
1905
|
-
|
|
1950
|
+
```javascript
|
|
1951
|
+
Product.search({
|
|
1952
|
+
conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
|
|
1953
|
+
sort: { attribute: 'id' },
|
|
1954
|
+
});
|
|
1955
|
+
```
|
|
1906
1956
|
|
|
1907
|
-
|
|
1957
|
+
- Pass `allowFullScan: true` to permit an unconditional ordered scan, or omit `sort` entirely to iterate without an index requirement.
|
|
1908
1958
|
|
|
1909
|
-
|
|
1959
|
+
7. **Paginate with `limit` and `offset`**:
|
|
1910
1960
|
|
|
1911
1961
|
```javascript
|
|
1912
|
-
Product.search({
|
|
1913
|
-
conditions: [{ attribute: 'id', comparator: 'greater_than', value: '' }],
|
|
1914
|
-
sort: { attribute: 'id' },
|
|
1915
|
-
});
|
|
1962
|
+
Product.search({ conditions: [...], limit: 20, offset: 40 });
|
|
1916
1963
|
```
|
|
1917
1964
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
8. **Debug query planning with `explain` and `enforceExecutionOrder`**:
|
|
1965
|
+
8. **Debug with `explain` and `enforceExecutionOrder`**:
|
|
1921
1966
|
- `explain: true` — returns conditions reordered as Harper will execute them.
|
|
1922
|
-
- `enforceExecutionOrder: true` — forces conditions to execute in the order supplied, disabling automatic re-ordering.
|
|
1967
|
+
- `enforceExecutionOrder: true` — forces conditions to execute in the order supplied, disabling Harper's automatic re-ordering.
|
|
1923
1968
|
|
|
1924
|
-
9. **Use `addTo` for concurrent-safe
|
|
1969
|
+
9. **Use `addTo` for concurrent-safe increments**: `addTo(property, value)` uses CRDT incrementation, safe across threads and nodes.
|
|
1925
1970
|
|
|
1926
1971
|
```javascript
|
|
1927
|
-
|
|
1928
|
-
|
|
1972
|
+
static async post(target, data) {
|
|
1973
|
+
const record = await this.update(target.id);
|
|
1974
|
+
record.addTo('quantity', -1);
|
|
1975
|
+
}
|
|
1929
1976
|
```
|
|
1930
1977
|
|
|
1931
|
-
10. **Wrap background work in `transaction()`**:
|
|
1978
|
+
10. **Wrap background work in `transaction()`**: Harper auto-starts transactions for HTTP handlers. Use `transaction()` explicitly for timers, background jobs, or any code outside a natural transaction context.
|
|
1932
1979
|
|
|
1933
1980
|
```javascript
|
|
1934
1981
|
await transaction(async (txn) => {
|
|
@@ -1940,18 +1987,18 @@ Apply this rule when writing server-side Harper component code that reads from o
|
|
|
1940
1987
|
|
|
1941
1988
|
The `txn` object exposes:
|
|
1942
1989
|
|
|
1943
|
-
| Member | Description |
|
|
1944
|
-
| --------------------- | ------------------------------------------------------ |
|
|
1945
|
-
| `commit()` | Commits the current transaction |
|
|
1946
|
-
| `abort()` | Aborts and resets
|
|
1947
|
-
| `resetReadSnapshot()` | Resets the read snapshot to the latest committed state |
|
|
1948
|
-
| `timestamp` | Timestamp associated with the current transaction |
|
|
1990
|
+
| Member | Type | Description |
|
|
1991
|
+
| --------------------- | --------------- | ------------------------------------------------------ |
|
|
1992
|
+
| `commit()` | `() => Promise` | Commits the current transaction |
|
|
1993
|
+
| `abort()` | `() => void` | Aborts the transaction and resets it |
|
|
1994
|
+
| `resetReadSnapshot()` | `() => void` | Resets the read snapshot to the latest committed state |
|
|
1995
|
+
| `timestamp` | `number` | Timestamp associated with the current transaction |
|
|
1949
1996
|
|
|
1950
|
-
|
|
1997
|
+
On normal callback completion the transaction commits automatically. If the callback throws, the transaction is aborted.
|
|
1951
1998
|
|
|
1952
|
-
|
|
1999
|
+
11. **Understand atomicity**: Transactions span a single database. All tables within the same database share one transactional context — reads return a consistent snapshot and writes across multiple tables commit atomically. Cross-database operations get separate transactions with no cross-database atomicity guarantee.
|
|
1953
2000
|
|
|
1954
|
-
|
|
2001
|
+
12. **Keep `harper` external when bundling for SSR**: In `vite.config`, mark `harper` as external so it resolves to the runtime rather than being bundled.
|
|
1955
2002
|
|
|
1956
2003
|
```javascript
|
|
1957
2004
|
// vite.config
|
|
@@ -1962,7 +2009,7 @@ Apply this rule when writing server-side Harper component code that reads from o
|
|
|
1962
2009
|
|
|
1963
2010
|
#### Examples
|
|
1964
2011
|
|
|
1965
|
-
|
|
2012
|
+
**Full CRUD flow:**
|
|
1966
2013
|
|
|
1967
2014
|
```javascript
|
|
1968
2015
|
import { tables } from 'harper';
|
|
@@ -1974,19 +2021,18 @@ const created = await Product.create({ name: 'Shirt', price: 9.5 });
|
|
|
1974
2021
|
// Patch
|
|
1975
2022
|
await Product.patch(created.id, { price: Math.round(created.price * 0.8 * 100) / 100 });
|
|
1976
2023
|
|
|
1977
|
-
//
|
|
2024
|
+
// Get by primary key
|
|
1978
2025
|
const record = await Product.get(created.id);
|
|
1979
2026
|
|
|
1980
|
-
//
|
|
1981
|
-
const
|
|
2027
|
+
// Search with conditions
|
|
2028
|
+
for await (const record of Product.search({
|
|
1982
2029
|
conditions: [{ attribute: 'price', comparator: 'less_than', value: 8.0 }],
|
|
1983
|
-
}
|
|
1984
|
-
for await (const record of Product.search(query)) {
|
|
2030
|
+
})) {
|
|
1985
2031
|
// process record
|
|
1986
2032
|
}
|
|
1987
2033
|
```
|
|
1988
2034
|
|
|
1989
|
-
|
|
2035
|
+
**Nested conditions with `or`:**
|
|
1990
2036
|
|
|
1991
2037
|
```javascript
|
|
1992
2038
|
Product.search({
|
|
@@ -2003,13 +2049,13 @@ Product.search({
|
|
|
2003
2049
|
});
|
|
2004
2050
|
```
|
|
2005
2051
|
|
|
2006
|
-
|
|
2052
|
+
**Chained attribute reference (join):**
|
|
2007
2053
|
|
|
2008
2054
|
```javascript
|
|
2009
2055
|
Product.search({ conditions: [{ attribute: ['brand', 'name'], value: 'Harper' }] });
|
|
2010
2056
|
```
|
|
2011
2057
|
|
|
2012
|
-
|
|
2058
|
+
**Background job with `transaction()`:**
|
|
2013
2059
|
|
|
2014
2060
|
```javascript
|
|
2015
2061
|
import { isMainThread } from 'node:worker_threads';
|
|
@@ -2037,7 +2083,7 @@ if (isMainThread) {
|
|
|
2037
2083
|
}
|
|
2038
2084
|
```
|
|
2039
2085
|
|
|
2040
|
-
|
|
2086
|
+
**SSR with Harper data:**
|
|
2041
2087
|
|
|
2042
2088
|
```typescript
|
|
2043
2089
|
import { tables } from 'harper';
|
|
@@ -2050,10 +2096,12 @@ export async function render(url: string): Promise<string> {
|
|
|
2050
2096
|
|
|
2051
2097
|
#### Notes
|
|
2052
2098
|
|
|
2053
|
-
- `tables` and `databases`
|
|
2054
|
-
-
|
|
2055
|
-
- `
|
|
2056
|
-
-
|
|
2099
|
+
- `tables` and `databases` calls run in a trusted server-side context and do **not** automatically apply the target table's role permissions.
|
|
2100
|
+
- Programmatic `update`, `patch`, and `delete` calls operate directly on stored data. Scope destructive operations with specific `conditions`, validate the affected set before writing, and gate them behind authorization controls.
|
|
2101
|
+
- If `transaction()` is called with a context that already has an active transaction, it reuses that transaction — making it safe to call defensively.
|
|
2102
|
+
- Always `await` the `transaction()` call and `catch` errors when outside a request context; an unawaited call means a failed write is never observed.
|
|
2103
|
+
- Guard against timer overlap: if a job can outlast its interval, use a `running` flag to skip a tick rather than opening two transactions over the same rows.
|
|
2104
|
+
- CommonJS is also supported: `const { tables, Resource } = require('harper');`
|
|
2057
2105
|
|
|
2058
2106
|
### 3.4 TypeScript Type Stripping in Harper
|
|
2059
2107
|
|
|
@@ -2272,22 +2320,25 @@ await fetch('http://localhost:9926/JokeCache/1', {
|
|
|
2272
2320
|
|
|
2273
2321
|
### 4.1 Deploying to Harper Fabric
|
|
2274
2322
|
|
|
2275
|
-
Instructions for the agent to follow when deploying a Harper application to a remote Harper
|
|
2323
|
+
Instructions for the agent to follow when deploying a Harper application to a Harper Fabric cluster, or any remote Harper instance, with the `harper` CLI, and when checking, staging, or going back to a release.
|
|
2276
2324
|
|
|
2277
2325
|
#### When to Use
|
|
2278
2326
|
|
|
2279
|
-
Apply this rule when deploying a
|
|
2327
|
+
Apply this rule when deploying to a remote cluster, choosing how it restarts, reading a deploy's `certification`, staging a release to make live later, or rolling back to a previous release. To deploy from CI with no stored credential, see [deploying-from-ci.md](deploying-from-ci.md). To set up the cluster first, see [creating-a-fabric-account-and-cluster.md](creating-a-fabric-account-and-cluster.md).
|
|
2280
2328
|
|
|
2281
2329
|
#### How It Works
|
|
2282
2330
|
|
|
2283
|
-
1. **
|
|
2331
|
+
1. **Log in to the cluster once**: Pass the cluster's **Application URL**, from its **Config → Overview** page, to `harper login`. The CLI stores the token, so later commands don't repeat credentials. If the current directory already has a `.env` file that sets no target, it also appends `HARPER_CLI_TARGET` to it.
|
|
2284
2332
|
|
|
2285
2333
|
```bash
|
|
2286
2334
|
harper login <Application URL>
|
|
2287
2335
|
# Provide cluster username and password when prompted
|
|
2288
2336
|
```
|
|
2289
2337
|
|
|
2290
|
-
|
|
2338
|
+
- For CI/CD on GitHub Actions, use workload identity (OIDC) instead, so the pipeline stores no Harper credential. From v5.4.0, `harper deploy setup=true provider=github-actions` sets it up in one command: see [deploying-from-ci.md](deploying-from-ci.md). Elsewhere, give the pipeline a refresh token from `harper login --for-ci` rather than a password.
|
|
2339
|
+
- For a one-off command, `auth_username=` and `auth_password=` also work, and take precedence over environment variables and saved login tokens. Don't use them in production.
|
|
2340
|
+
|
|
2341
|
+
2. **Deploy with a restart**: Run `harper deploy`, with `restart=true` or `restart=rolling` so the release is certified before it serves (step 3).
|
|
2291
2342
|
|
|
2292
2343
|
```bash
|
|
2293
2344
|
harper deploy \
|
|
@@ -2298,100 +2349,123 @@ Apply this rule when deploying a Harper application to a remote Harper instance
|
|
|
2298
2349
|
replicated=true
|
|
2299
2350
|
```
|
|
2300
2351
|
|
|
2301
|
-
|
|
2352
|
+
- On Harper Pro and Fabric, a deploy goes to every node in the cluster unless you pass `replicated=false`. Harper core on its own does not replicate.
|
|
2353
|
+
- `project` defaults to the current directory's name for a directory deploy, or is derived from the package for a package deploy.
|
|
2354
|
+
- A bare `target` host defaults to `https://<host>:9925`.
|
|
2355
|
+
- Pass `json=true` to print the result as JSON instead of YAML.
|
|
2356
|
+
- Every result carries a `deployment_id`, staged or not. It names this release when you go back to it later (step 4).
|
|
2302
2357
|
|
|
2303
|
-
|
|
2304
|
-
| ----------------------- | ------------------------------------------------------------ |
|
|
2305
|
-
| Current local directory | Omit `package` |
|
|
2306
|
-
| npm package | `package="@harperdb/status-check"` |
|
|
2307
|
-
| GitHub (shorthand) | `package="HarperFast/status-check"` |
|
|
2308
|
-
| GitHub (URL) | `package="https://github.com/HarperFast/status-check"` |
|
|
2309
|
-
| Private repo (SSH) | `package="git+ssh://git@github.com:HarperDB/secret-app.git"` |
|
|
2310
|
-
| Tarball | `package="https://example.com/application.tar.gz"` |
|
|
2358
|
+
3. **Choose the restart, and read `certification`**: `restart=true` restarts Harper after deploying. On a cluster, use `restart=rolling`, the staggered, zero-downtime restart: the command exits `0` once the node you called has taken the release and started the rolling restart's job, and returns its `restartJobId`.
|
|
2311
2359
|
|
|
2312
|
-
|
|
2360
|
+
Both certify the release in a canary worker before rolling it out. The canary replaces worker 0 and loads every component, but is held out of traffic until it reports whether the deployed component loaded. Until then, the workers already running keep serving the previous release. A deploy that succeeds, and is not staged, reports the result in `certification`:
|
|
2313
2361
|
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2362
|
+
| `certification` | Meaning |
|
|
2363
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2364
|
+
| `certified` | The canary loaded the release, and the rollout went on. |
|
|
2365
|
+
| `uncertified` | The rollout went on unchecked: the component ran nothing as it loaded (`loadComponent: dev-only`, an absent `if-installed` component, or safe mode), or no worker was replaced. |
|
|
2366
|
+
| `unavailable` | The deploy restarted unchecked: no worker on this node loads the component, its `isolated` setting changed, it was already live, or it was deployed from a local directory (`package: file:<dir>`). |
|
|
2367
|
+
| `not-requested` | The deploy did not restart. |
|
|
2368
|
+
- **A rejected release fails the deploy.** If the canary throws while loading the component, exits, or doesn't report within a minute, the deploy fails with `400`. The release it replaced is made live again, and nothing is replicated to other nodes. The error's `certification` is an object whose `status` is `rejected`, or `interrupted` for a restart that stopped before the canary decided.
|
|
2369
|
+
- **A release with nothing to go back to fails closed.** A rejected first deploy, or one whose previous release was not kept, stays on disk, but no worker on that node loads it, even after Harper restarts. `get_status` reports the component as failed. Deploy a fix, or once the cause is fixed, activate the same `deployment_id` again with `restart`.
|
|
2370
|
+
- **A rolling deploy exits before every node has taken the release.** The other nodes stage it, and the job activates it on each in turn, each with a canary of its own. A node that rejects it keeps the previous release, or fails closed if it had none. Nothing is rolled back on the others, and the job fails naming each node that did not activate it. The deployment record reports `success` while the job runs and after it fails, so poll the job instead: `harper get_job id=<restartJobId>` ends `COMPLETE`, or `ERROR` with a `message` listing each node's outcome under `activated`.
|
|
2371
|
+
- **Poll the node that received the deploy.** A job is recorded only on the node that ran it, so any other node answers `get_job` with an empty array. A role with an `operations` allowlist, or a token with an `operations` scope, must list `get_job` to poll it.
|
|
2372
|
+
- Another deploy or `drop_component` of the component on that node is refused with `409` until the rollout has finished.
|
|
2373
|
+
- A node running a version before v5.4.0 activates the release without a canary.
|
|
2317
2374
|
|
|
2318
|
-
4. **
|
|
2375
|
+
4. **Go back to a previous release by its `deployment_id`**: When a deploy replaces the live release, Harper keeps the replaced one under the `deployment_id` that deployed it. Activating that id puts back the exact installed release with no fetch, rebuild, or reinstall, where deploying an older commit installs it again from source. Find the id with `list_deployments`, then deploy it. On a cluster, restart with `rolling` and poll its job (step 3):
|
|
2319
2376
|
|
|
2320
2377
|
```bash
|
|
2321
|
-
harper
|
|
2378
|
+
harper list_deployments project=my-app status=success
|
|
2379
|
+
harper deploy project=my-app deployment_id=<id> restart=rolling
|
|
2322
2380
|
```
|
|
2323
2381
|
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
# Roll back by deploying an older commit
|
|
2333
|
-
harper deploy ref=9f8c2a1 restart=true replicated=true
|
|
2382
|
+
```json
|
|
2383
|
+
{
|
|
2384
|
+
"operation": "deploy_component",
|
|
2385
|
+
"project": "my-app",
|
|
2386
|
+
"deployment_id": "<id of the deployment you want back>",
|
|
2387
|
+
"restart": "rolling"
|
|
2388
|
+
}
|
|
2334
2389
|
```
|
|
2335
2390
|
|
|
2336
|
-
|
|
2337
|
-
-
|
|
2338
|
-
-
|
|
2339
|
-
-
|
|
2340
|
-
-
|
|
2341
|
-
-
|
|
2391
|
+
- `list_deployments` returns records newest first. Filter by `project` and `status`; omit `limit` to return every matching record. A deploy in progress reads `pending`, or `loading`, until its `completed_at` is set.
|
|
2392
|
+
- The newest `success` is not necessarily the release every node is serving: a rolling deploy's record says `success` before the other nodes have taken the release, and keeps saying it if one rejects it.
|
|
2393
|
+
- Redeploy an older commit by `ref` (step 7) only when the release is no longer kept.
|
|
2394
|
+
- The activation restores the root config entry that deployment published. The release it replaces is kept in turn, so you can go forward again the same way.
|
|
2395
|
+
- Kept releases and staged builds count against `deployment.stagingRetention.maxCount` (default `5`). Each kept release is a full installed copy of the component, `node_modules` included, so budget disk for it. `0` keeps no replaced release.
|
|
2396
|
+
- A release made live before v5.3.0, or deployed from a `file:` directory, is not kept.
|
|
2397
|
+
- Each node answers for itself. A node that never had the release, or no longer keeps it, answers `404`, which `deploy_component` reports as a failed peer. `409` means the build is there but can't be activated.
|
|
2342
2398
|
|
|
2343
|
-
5. **
|
|
2399
|
+
5. **Stage a release to make live later**: `activate=false` (`activate: false` in the operation) builds and installs the release on each node without making it live, so the slow, fallible part happens when you choose and the cut-over is quick. The result's `deployment_id` names the staged build:
|
|
2344
2400
|
|
|
2345
|
-
```
|
|
2346
|
-
|
|
2401
|
+
```json
|
|
2402
|
+
{
|
|
2403
|
+
"operation": "deploy_component",
|
|
2404
|
+
"project": "my-app",
|
|
2405
|
+
"package": "npm:@my-org/my-app@1.2.3",
|
|
2406
|
+
"activate": false
|
|
2407
|
+
}
|
|
2347
2408
|
```
|
|
2348
2409
|
|
|
2349
|
-
|
|
2410
|
+
```json
|
|
2411
|
+
{
|
|
2412
|
+
"deployment_id": "a3f8c2d1-...",
|
|
2413
|
+
"message": "Staged: my-app. Deploy it with deploy_component deployment_id=a3f8c2d1-..."
|
|
2414
|
+
}
|
|
2415
|
+
```
|
|
2350
2416
|
|
|
2351
|
-
|
|
2417
|
+
Make it live by deploying that `deployment_id`, as in step 4. Nothing changes until then, and `get_deployment` reports it as `staged`.
|
|
2418
|
+
- Pass `restart` on the activation, not the stage: `restart` is rejected alongside `activate: false`.
|
|
2419
|
+
- The activation takes no `package`, `payload`, `credentials`, install options, or `urlPath`/`host`. They are rejected, because the staged build already decided them.
|
|
2420
|
+
- Activating the release that is already live succeeds without a swap, so if an activation fails partway (a peer was unreachable, or the root config entry wasn't written), retry the same `deployment_id`. A node that already switched answers success, one still holding the build switches now, and one that holds neither answers `404`; the retry also finishes what the failed attempt left half done. With `restart`, a node where the release is already live still restarts every worker, isolated applications' included. Without it, the node marks the component as needing a restart.
|
|
2421
|
+
- A staged build outlives later deploys: stage v2, deploy v3, and activating v2's id returns the component to v2.
|
|
2422
|
+
- `file:` directory sources cannot be staged.
|
|
2423
|
+
- Upgrade every node before staging. A node before v5.3.0 doesn't recognize `activate: false`, and serves the release immediately. A peer that doesn't confirm staging fails the operation with the node names.
|
|
2352
2424
|
|
|
2353
|
-
|
|
2354
|
-
harper deploy setup=true
|
|
2355
|
-
```
|
|
2425
|
+
6. **Choose a package source**: The `package` field accepts any valid npm dependency value:
|
|
2356
2426
|
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2427
|
+
| Source | `package` value |
|
|
2428
|
+
| ----------------------- | --------------------------------------------------------------------------------------------- |
|
|
2429
|
+
| Current local directory | Omit `package` |
|
|
2430
|
+
| npm package | `package="@harperdb/status-check"` |
|
|
2431
|
+
| GitHub (public) | `package="HarperFast/status-check"` or `package="https://github.com/HarperFast/status-check"` |
|
|
2432
|
+
| Private repo (SSH) | `package="git+ssh://git@github.com:HarperDB/secret-app.git"` |
|
|
2433
|
+
| Tarball | `package="https://example.com/application.tar.gz"` |
|
|
2363
2434
|
|
|
2364
|
-
|
|
2435
|
+
For git tags, use the `semver` directive: `HarperFast/application-template#semver:v1.0.0`. For an SSH-based private repo, register the key with `add_ssh_key` first.
|
|
2365
2436
|
|
|
2366
|
-
7. **
|
|
2437
|
+
7. **Deploy by reference for a reproducible source**: `by_ref=true` sends a pinned git reference instead of uploading the working directory, and the cluster fetches and builds that exact commit. It resolves the repository's `origin` remote and the current commit, then deploys `package=git+https://github.com/<owner>/<repo>.git#<full commit SHA>`. Pass `ref` to deploy a specific tag or commit instead of `HEAD`; it implies `by_ref`.
|
|
2367
2438
|
|
|
2368
2439
|
```bash
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
harper deploy
|
|
2372
|
-
project=<name> \
|
|
2373
|
-
package=<package> \
|
|
2374
|
-
target=<remote> \
|
|
2375
|
-
restart=true \
|
|
2376
|
-
replicated=true
|
|
2440
|
+
harper deploy by_ref=true restart=true replicated=true
|
|
2441
|
+
harper deploy ref=v1.2.0 restart=true replicated=true
|
|
2442
|
+
harper deploy ref=9f8c2a1 restart=true replicated=true
|
|
2377
2443
|
```
|
|
2378
2444
|
|
|
2379
|
-
|
|
2445
|
+
- A `ref` is resolved to a full commit SHA before the deploy is sent, from the local checkout or the remote. A full commit SHA is accepted directly. If a `ref` can't be resolved, the deploy stops: run `git fetch` and retry, or pass a full SHA.
|
|
2446
|
+
- Any other `ref` must name something a clone can fetch: `refs/heads/*` or `refs/tags/*`, or a bare branch or tag name. A qualified ref outside those, such as `refs/pull/123/head`, is rejected up front.
|
|
2447
|
+
- Commit and push first: the cluster clones from the remote. `by_ref` warns when the working tree is dirty, and when the commit isn't on any remote branch. The second check is skipped under GitHub Actions.
|
|
2448
|
+
- On a GitHub Actions `pull_request` run, `by_ref` deploys the pull request's head commit. If the event payload isn't readable, the deploy stops; pass the commit explicitly.
|
|
2449
|
+
- The cluster installs and builds from source on each node, so commit your lockfile if the build must be reproducible. If the build can't run on the node, deploy the built output as a payload instead.
|
|
2450
|
+
|
|
2451
|
+
8. **Deploy a private repository by reference**: Pass `credential=true` with `by_ref=true`. The CLI attaches a `credentials` reference, and the cluster resolves the secret in memory at clone time, so no token travels in the operation body or lands on disk.
|
|
2380
2452
|
|
|
2381
2453
|
```bash
|
|
2382
|
-
harper deploy
|
|
2383
|
-
project=<name> \
|
|
2384
|
-
package=<package> \
|
|
2385
|
-
auth_username=<username> \
|
|
2386
|
-
auth_password=<password> \
|
|
2387
|
-
target=<remote> \
|
|
2388
|
-
restart=true \
|
|
2389
|
-
replicated=true
|
|
2454
|
+
harper deploy by_ref=true credential=true restart=true replicated=true
|
|
2390
2455
|
```
|
|
2391
2456
|
|
|
2457
|
+
Provision that credential once per component and source with `harper deploy setup=true`. (With `provider=github-actions`, it sets up GitHub Actions deploys instead: see [deploying-from-ci.md](deploying-from-ci.md).) It is interactive, and its operations require **super_user**, so run it with an administrative credential, not the CI identity it provisions for. It:
|
|
2458
|
+
1. Fetches the cluster's public key with `get_secrets_public_key`.
|
|
2459
|
+
2. Encrypts the token locally into an `enc:v1:` envelope, so the plaintext never leaves your machine.
|
|
2460
|
+
3. Stores only the ciphertext with `set_secret`, scoped to the component, never in the global `processEnv` tier.
|
|
2461
|
+
4. Grants the component permission to resolve it with `grant_secret`.
|
|
2462
|
+
5. Prints the `credentials` reference for the deploy to use.
|
|
2463
|
+
|
|
2464
|
+
Use a **fine-grained** personal access token with **Contents: Read-only** on that one repository. Avoid the `gh` CLI's session token: it typically carries `repo`, `read:org`, `gist`, and `workflow` scopes across your whole account. The sealed token is durable, and later deploys reuse it.
|
|
2465
|
+
|
|
2392
2466
|
#### Examples
|
|
2393
2467
|
|
|
2394
|
-
**
|
|
2468
|
+
**Deploy after logging in, and wait for a rolling restart:**
|
|
2395
2469
|
|
|
2396
2470
|
```bash
|
|
2397
2471
|
harper login https://my-cluster.harperdbcloud.com
|
|
@@ -2399,57 +2473,370 @@ harper deploy \
|
|
|
2399
2473
|
project=my-app \
|
|
2400
2474
|
package="HarperFast/my-app" \
|
|
2401
2475
|
target=https://my-cluster.harperdbcloud.com \
|
|
2402
|
-
restart=
|
|
2403
|
-
|
|
2476
|
+
restart=rolling \
|
|
2477
|
+
json=true > deploy.json
|
|
2478
|
+
# Poll the node that received the deploy until the job is COMPLETE or ERROR
|
|
2479
|
+
harper get_job id=<restartJobId from deploy.json> json=true
|
|
2404
2480
|
```
|
|
2405
2481
|
|
|
2406
|
-
**
|
|
2482
|
+
**Roll back to the release a deploy replaced, node by node:**
|
|
2407
2483
|
|
|
2408
2484
|
```bash
|
|
2409
|
-
harper
|
|
2485
|
+
harper list_deployments project=my-app status=success
|
|
2486
|
+
harper deploy project=my-app deployment_id=<previous deployment_id> restart=rolling json=true
|
|
2487
|
+
# Then poll its restartJobId with get_job, as above
|
|
2410
2488
|
```
|
|
2411
2489
|
|
|
2412
|
-
**
|
|
2490
|
+
**Stage a release now, and make it live in a maintenance window:**
|
|
2413
2491
|
|
|
2414
2492
|
```bash
|
|
2415
|
-
|
|
2493
|
+
harper deploy project=my-app package=npm:@my-org/my-app@1.2.3 activate=false
|
|
2494
|
+
# Later, with the deployment_id the stage returned
|
|
2495
|
+
harper deploy project=my-app deployment_id=<staged deployment_id> restart=rolling
|
|
2496
|
+
```
|
|
2497
|
+
|
|
2498
|
+
**Deploy a pull request's head commit by reference in GitHub Actions:**
|
|
2499
|
+
|
|
2500
|
+
```bash
|
|
2501
|
+
harper deploy ref=${{ github.event.pull_request.head.sha }} restart=true replicated=true
|
|
2502
|
+
```
|
|
2503
|
+
|
|
2504
|
+
**Deploy a private repository by reference:**
|
|
2505
|
+
|
|
2506
|
+
```bash
|
|
2507
|
+
# Once, as super_user
|
|
2416
2508
|
harper deploy setup=true
|
|
2417
2509
|
|
|
2418
|
-
#
|
|
2510
|
+
# Every deploy
|
|
2419
2511
|
harper deploy by_ref=true credential=true restart=true replicated=true
|
|
2420
2512
|
```
|
|
2421
2513
|
|
|
2422
|
-
|
|
2514
|
+
### 4.2 Deploying a Harper Application from GitHub Actions with OIDC
|
|
2515
|
+
|
|
2516
|
+
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.
|
|
2517
|
+
|
|
2518
|
+
#### When to Use
|
|
2519
|
+
|
|
2520
|
+
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.
|
|
2521
|
+
|
|
2522
|
+
#### How It Works
|
|
2523
|
+
|
|
2524
|
+
##### One-Command Setup (Branch-Based Deploys)
|
|
2525
|
+
|
|
2526
|
+
1. **Run setup once** from the project checkout, signed in as a `super_user`:
|
|
2527
|
+
|
|
2528
|
+
```bash
|
|
2529
|
+
harper login https://my-cluster.example.com:9925
|
|
2530
|
+
harper deploy setup=true provider=github-actions project=my-app
|
|
2531
|
+
```
|
|
2532
|
+
|
|
2533
|
+
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:
|
|
2534
|
+
|
|
2535
|
+
```bash
|
|
2536
|
+
harper deploy setup=true provider=github-actions project=my-app workflow=.github/workflows/deploy.yml
|
|
2537
|
+
```
|
|
2538
|
+
|
|
2539
|
+
2. **Understand what setup does** — in order:
|
|
2540
|
+
- 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`).
|
|
2541
|
+
- **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.
|
|
2542
|
+
- 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>`.
|
|
2543
|
+
- Reads the policy back and compares it claim by claim with what it intended to write.
|
|
2544
|
+
- Sets the repository's `HARPER_CLI_TARGET` Actions variable via `gh` if one does not already exist.
|
|
2545
|
+
|
|
2546
|
+
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:
|
|
2547
|
+
|
|
2548
|
+
```bash
|
|
2549
|
+
harper drop_oidc_trust id=github-actions-<project>
|
|
2550
|
+
harper deploy setup=true provider=github-actions project=my-app
|
|
2551
|
+
```
|
|
2552
|
+
|
|
2553
|
+
##### Manual Setup (Tag-Based or Custom Deploys)
|
|
2554
|
+
|
|
2555
|
+
Use this path when deploying on version tags, because a branch-pinned policy cannot match a tag ref.
|
|
2556
|
+
|
|
2557
|
+
1. **Create a deploy-only role and user**:
|
|
2558
|
+
|
|
2559
|
+
```bash
|
|
2560
|
+
harper add_role role=ci_deploy permission='{"operations":["deploy_component","get_job","get_deployment","registration_info"]}'
|
|
2561
|
+
harper add_user username=ci-deploy role=ci_deploy active=true password="$(openssl rand -base64 32)"
|
|
2562
|
+
```
|
|
2563
|
+
|
|
2564
|
+
- `deploy_component` — the deploy itself.
|
|
2565
|
+
- `get_job` — required for the wait step; an `operations` allowlist refuses anything not listed, so without it the wait step fails with `403`.
|
|
2566
|
+
- `get_deployment` — reads the deployment record.
|
|
2567
|
+
- `registration_info` — version check `harper deploy` makes before uploading.
|
|
2568
|
+
- `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.
|
|
2569
|
+
|
|
2570
|
+
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):
|
|
2571
|
+
|
|
2572
|
+
```bash
|
|
2573
|
+
harper add_oidc_trust \
|
|
2574
|
+
id=my-app-release \
|
|
2575
|
+
issuer=https://token.actions.githubusercontent.com \
|
|
2576
|
+
audience=https://my-cluster.example.com:9925/ \
|
|
2577
|
+
user=ci-deploy \
|
|
2578
|
+
claims='{"repository_id":"67890","workflow_path":"my-org/my-app/.github/workflows/deploy.yml","environment":"production"}'
|
|
2579
|
+
```
|
|
2580
|
+
|
|
2581
|
+
Key claim rules:
|
|
2582
|
+
- `repository_id` is immutable — survives renames. Look it up with `gh api repos/my-org/my-app --jq .id`.
|
|
2583
|
+
- `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>`.
|
|
2584
|
+
- `environment` is the ref gate. Without it, anyone who can push a branch can mint a token.
|
|
2585
|
+
- `audience` must be the exact target URL with its port and a **trailing slash**.
|
|
2586
|
+
- 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.
|
|
2587
|
+
|
|
2588
|
+
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.
|
|
2589
|
+
|
|
2590
|
+
4. **Store the target as a repository variable** (not a secret — it is not sensitive):
|
|
2591
|
+
|
|
2592
|
+
```bash
|
|
2593
|
+
gh variable set HARPER_CLI_TARGET --repo my-org/my-app --body https://my-cluster.example.com:9925/
|
|
2594
|
+
```
|
|
2595
|
+
|
|
2596
|
+
##### How OIDC Authentication Works in the Workflow
|
|
2597
|
+
|
|
2598
|
+
Once a trust policy exists, every `harper` command in the deploy job authenticates through it automatically when all four conditions hold:
|
|
2599
|
+
|
|
2600
|
+
1. **The job may request an identity token.** On GitHub Actions that is `permissions: id-token: write` on the job.
|
|
2601
|
+
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.
|
|
2602
|
+
3. **The run matches the policy's claims**: its repository, its workflow file and branch, and its environment.
|
|
2603
|
+
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.
|
|
2604
|
+
|
|
2605
|
+
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`.
|
|
2606
|
+
|
|
2607
|
+
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.
|
|
2608
|
+
|
|
2609
|
+
Nothing in the workflow names the policy. The CLI prints to stderr which policy authenticated it and as whom:
|
|
2610
|
+
|
|
2611
|
+
```
|
|
2612
|
+
Requesting a GitHub Actions identity token for https://my-instance.harperdb.io:9925/...
|
|
2613
|
+
Authenticated as 'ci-deploy' via OIDC trust policy 'my-app-prod'.
|
|
2614
|
+
```
|
|
2615
|
+
|
|
2616
|
+
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.
|
|
2617
|
+
|
|
2618
|
+
##### Policy Specificity for GitHub Actions
|
|
2619
|
+
|
|
2620
|
+
For `https://token.actions.githubusercontent.com`, a policy must satisfy all three of these:
|
|
2621
|
+
|
|
2622
|
+
| Requirement | Satisfied by one of | Left open otherwise |
|
|
2623
|
+
| ---------------------- | ------------------------------------------------------------------------ | ----------------------------------------------- |
|
|
2624
|
+
| **Pin the repository** | `repository_id`, `repository` | Any repository |
|
|
2625
|
+
| **Pin the workflow** | `workflow_ref`, `workflow_path`, `job_workflow_ref`, `job_workflow_path` | Any workflow in that repository |
|
|
2626
|
+
| **Gate the ref** | `workflow_ref`, `ref`, `environment` | Any branch that can be pushed to the repository |
|
|
2627
|
+
|
|
2628
|
+
Additional constraints:
|
|
2629
|
+
|
|
2630
|
+
- `repository_id` is preferred over `repository` — it is immutable and survives renames.
|
|
2631
|
+
- 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`.
|
|
2632
|
+
- `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`.
|
|
2633
|
+
|
|
2634
|
+
##### Revoking Access
|
|
2635
|
+
|
|
2636
|
+
- **Stop future runs**: `harper drop_oidc_trust id=github-actions-<project>` (does not revoke already-issued tokens, which are valid up to one hour).
|
|
2637
|
+
- **Stop an already-issued token**: `harper alter_user username=<project>-ci-deploy active=false`.
|
|
2638
|
+
- **No CI secret to rotate** — there is none.
|
|
2639
|
+
|
|
2640
|
+
#### Examples
|
|
2641
|
+
|
|
2642
|
+
##### Path A: Deploy a Tagged Release (`v1.2.3`) from the Repository
|
|
2643
|
+
|
|
2644
|
+
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.
|
|
2645
|
+
|
|
2646
|
+
```yaml
|
|
2647
|
+
# .github/workflows/deploy.yml
|
|
2648
|
+
name: Deploy to Harper
|
|
2649
|
+
on:
|
|
2650
|
+
push:
|
|
2651
|
+
tags: ['v*']
|
|
2652
|
+
|
|
2653
|
+
jobs:
|
|
2654
|
+
deploy:
|
|
2655
|
+
runs-on: ubuntu-latest
|
|
2656
|
+
environment: production
|
|
2657
|
+
permissions:
|
|
2658
|
+
contents: read
|
|
2659
|
+
id-token: write
|
|
2660
|
+
env:
|
|
2661
|
+
HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }}
|
|
2662
|
+
steps:
|
|
2663
|
+
- uses: actions/checkout@v4
|
|
2664
|
+
with:
|
|
2665
|
+
persist-credentials: false
|
|
2666
|
+
- uses: actions/setup-node@v4
|
|
2667
|
+
with:
|
|
2668
|
+
node-version: 22
|
|
2669
|
+
- run: npm install --global harper@^5.3
|
|
2670
|
+
- name: Deploy the tagged commit
|
|
2671
|
+
run: harper deploy project=my-app by_ref=true restart=rolling json=true > deploy.json
|
|
2672
|
+
- name: Wait for every node to take the release
|
|
2673
|
+
run: |
|
|
2674
|
+
JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
|
|
2675
|
+
[ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
|
|
2676
|
+
LAST="could not be read"
|
|
2677
|
+
for attempt in $(seq 60); do
|
|
2678
|
+
sleep 10
|
|
2679
|
+
harper get_job id="$JOB_ID" json=true > job.json || continue
|
|
2680
|
+
case $(jq -r '.[0].status // "MISSING"' job.json) in
|
|
2681
|
+
COMPLETE) exit 0 ;;
|
|
2682
|
+
ERROR) jq -r '.[0].message' job.json; exit 1 ;;
|
|
2683
|
+
MISSING) LAST="was not on the node that answered; its outcome is unknown" ;;
|
|
2684
|
+
*) LAST="was still running" ;;
|
|
2685
|
+
esac
|
|
2686
|
+
done
|
|
2687
|
+
echo "After 60 polls, job $JOB_ID $LAST"
|
|
2688
|
+
exit 1
|
|
2689
|
+
```
|
|
2690
|
+
|
|
2691
|
+
Key details:
|
|
2692
|
+
|
|
2693
|
+
- **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.
|
|
2694
|
+
- **`persist-credentials: false`** keeps the job's `GITHUB_TOKEN` out of `.git/config`.
|
|
2695
|
+
- **`harper@^5.3`** pins the CLI's major version. 5.3 is the first CLI that authenticates with the identity token.
|
|
2696
|
+
- **`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.
|
|
2697
|
+
- **`restart=rolling`** restarts nodes one at a time. Use `restart=true` for a single-node or dev instance.
|
|
2698
|
+
- **`json=true`** prints the deploy's result as JSON on stdout; progress and authentication messages go to stderr.
|
|
2699
|
+
- **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).
|
|
2700
|
+
- **`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.
|
|
2701
|
+
- **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.
|
|
2702
|
+
|
|
2703
|
+
##### Path B: Build in CI, Publish to GitHub Packages, Deploy the Artifact
|
|
2704
|
+
|
|
2705
|
+
Give `package.json` a scoped name and `"publishConfig": { "registry": "https://npm.pkg.github.com" }`:
|
|
2706
|
+
|
|
2707
|
+
```json
|
|
2708
|
+
{
|
|
2709
|
+
"name": "@my-org/my-app",
|
|
2710
|
+
"version": "1.2.3",
|
|
2711
|
+
"publishConfig": { "registry": "https://npm.pkg.github.com" },
|
|
2712
|
+
"files": ["config.yaml", "schema.graphql", "dist/"]
|
|
2713
|
+
}
|
|
2714
|
+
```
|
|
2715
|
+
|
|
2716
|
+
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`.
|
|
2717
|
+
|
|
2718
|
+
```yaml
|
|
2719
|
+
# .github/workflows/deploy.yml
|
|
2720
|
+
name: Build, publish, and deploy to Harper
|
|
2721
|
+
on:
|
|
2722
|
+
push:
|
|
2723
|
+
tags: ['v*']
|
|
2724
|
+
|
|
2725
|
+
jobs:
|
|
2726
|
+
publish:
|
|
2727
|
+
runs-on: ubuntu-latest
|
|
2728
|
+
permissions:
|
|
2729
|
+
contents: read
|
|
2730
|
+
packages: write
|
|
2731
|
+
steps:
|
|
2732
|
+
- uses: actions/checkout@v4
|
|
2733
|
+
with:
|
|
2734
|
+
persist-credentials: false
|
|
2735
|
+
- uses: actions/setup-node@v4
|
|
2736
|
+
with:
|
|
2737
|
+
node-version: 22
|
|
2738
|
+
registry-url: https://npm.pkg.github.com
|
|
2739
|
+
scope: '@my-org'
|
|
2740
|
+
- run: npm ci
|
|
2741
|
+
env:
|
|
2742
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
2743
|
+
- run: npm run build
|
|
2744
|
+
- run: npm test
|
|
2745
|
+
- name: Publish to GitHub Packages
|
|
2746
|
+
run: npm publish
|
|
2747
|
+
env:
|
|
2748
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
2749
|
+
|
|
2750
|
+
deploy:
|
|
2751
|
+
needs: publish
|
|
2752
|
+
runs-on: ubuntu-latest
|
|
2753
|
+
environment: production
|
|
2754
|
+
permissions:
|
|
2755
|
+
id-token: write
|
|
2756
|
+
env:
|
|
2757
|
+
HARPER_CLI_TARGET: ${{ vars.HARPER_CLI_TARGET }}
|
|
2758
|
+
steps:
|
|
2759
|
+
- uses: actions/setup-node@v4
|
|
2760
|
+
with:
|
|
2761
|
+
node-version: 22
|
|
2762
|
+
- run: npm install --global harper@^5.3
|
|
2763
|
+
- name: Deploy the published version
|
|
2764
|
+
run: |
|
|
2765
|
+
harper deploy project=my-app package="@my-org/my-app@${GITHUB_REF_NAME#v}" \
|
|
2766
|
+
credentials='[{"registry":"https://npm.pkg.github.com","scope":"@my-org","secret":"deploy.my-app.npm.pkg.github.com"}]' \
|
|
2767
|
+
restart=rolling json=true > deploy.json
|
|
2768
|
+
- name: Wait for every node to take the release
|
|
2769
|
+
run: |
|
|
2770
|
+
JOB_ID=$(jq -r '.restartJobId // empty' deploy.json)
|
|
2771
|
+
[ -n "$JOB_ID" ] || { echo "The rolling deploy returned no restartJobId"; exit 1; }
|
|
2772
|
+
LAST="could not be read"
|
|
2773
|
+
for attempt in $(seq 60); do
|
|
2774
|
+
sleep 10
|
|
2775
|
+
harper get_job id="$JOB_ID" json=true > job.json || continue
|
|
2776
|
+
case $(jq -r '.[0].status // "MISSING"' job.json) in
|
|
2777
|
+
COMPLETE) exit 0 ;;
|
|
2778
|
+
ERROR) jq -r '.[0].message' job.json; exit 1 ;;
|
|
2779
|
+
MISSING) LAST="was not on the node that answered; its outcome is unknown" ;;
|
|
2780
|
+
*) LAST="was still running" ;;
|
|
2781
|
+
esac
|
|
2782
|
+
done
|
|
2783
|
+
echo "After 60 polls, job $JOB_ID $LAST"
|
|
2784
|
+
exit 1
|
|
2785
|
+
```
|
|
2786
|
+
|
|
2787
|
+
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.
|
|
2788
|
+
|
|
2789
|
+
Credential roles:
|
|
2790
|
+
|
|
2791
|
+
- **Publishing** and installing private `@my-org` dependencies use the workflow's ephemeral `GITHUB_TOKEN` (`packages: write` includes read). Both happen entirely inside `publish`.
|
|
2792
|
+
- **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.
|
|
2793
|
+
|
|
2794
|
+
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:
|
|
2423
2795
|
|
|
2424
2796
|
```bash
|
|
2425
|
-
harper deploy
|
|
2797
|
+
harper deploy setup=true provider=github project=my-app
|
|
2798
|
+
# or for GitHub Packages:
|
|
2799
|
+
harper deploy setup=true provider=npm project=my-app registry=https://npm.pkg.github.com scope=@my-org
|
|
2426
2800
|
```
|
|
2427
2801
|
|
|
2428
|
-
**
|
|
2802
|
+
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.
|
|
2803
|
+
|
|
2804
|
+
##### Check the Deployment Record
|
|
2429
2805
|
|
|
2430
2806
|
```bash
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2807
|
+
harper get_deployment deployment_id="$(jq -r .deployment_id deploy.json)" json=true
|
|
2808
|
+
```
|
|
2809
|
+
|
|
2810
|
+
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.
|
|
2811
|
+
|
|
2812
|
+
##### List and Inspect Trust Policies
|
|
2813
|
+
|
|
2814
|
+
```bash
|
|
2815
|
+
harper list_oidc_trust
|
|
2816
|
+
```
|
|
2817
|
+
|
|
2818
|
+
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.
|
|
2819
|
+
|
|
2820
|
+
##### Drop a Trust Policy
|
|
2821
|
+
|
|
2822
|
+
```bash
|
|
2823
|
+
harper drop_oidc_trust id=github-actions-<project>
|
|
2439
2824
|
```
|
|
2440
2825
|
|
|
2441
2826
|
#### Notes
|
|
2442
2827
|
|
|
2443
|
-
-
|
|
2444
|
-
- `harper deploy
|
|
2445
|
-
-
|
|
2446
|
-
-
|
|
2447
|
-
-
|
|
2448
|
-
-
|
|
2449
|
-
-
|
|
2450
|
-
-
|
|
2828
|
+
- `HARPER_CLI_REFRESH_TOKEN` and any other stored credential outrank the OIDC exchange. Remove them from the workflow if switching to OIDC.
|
|
2829
|
+
- **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.
|
|
2830
|
+
- **Rotating the deploy credential**: run `harper deploy setup=true` again with the same name and a new value. The pipeline doesn't change.
|
|
2831
|
+
- **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).
|
|
2832
|
+
- 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.
|
|
2833
|
+
- The `audience` in `add_oidc_trust` must be the exact target URL with its port and a trailing slash.
|
|
2834
|
+
- 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.
|
|
2835
|
+
- `token.actions.githubusercontent.com` is the required `issuer` value for GitHub Actions OIDC policies.
|
|
2836
|
+
- Git-host credentials require git ≥ 2.31 on the Harper nodes and are not supported on Windows nodes. Registry credentials have no such constraints.
|
|
2837
|
+
- See [deploying-to-harper-fabric.md](deploying-to-harper-fabric.md) for cluster-level deployment context.
|
|
2451
2838
|
|
|
2452
|
-
### 4.
|
|
2839
|
+
### 4.3 Creating a Harper Fabric Account and Cluster
|
|
2453
2840
|
|
|
2454
2841
|
Follow these steps to set up your Harper Fabric environment for deployment.
|
|
2455
2842
|
|
|
@@ -2460,7 +2847,7 @@ Follow these steps to set up your Harper Fabric environment for deployment.
|
|
|
2460
2847
|
3. **Create a Cluster**: Create a new cluster. This can be on the free tier, no credit card required.
|
|
2461
2848
|
4. **Set Credentials**: During setup, set the cluster username and password to finish configuring it.
|
|
2462
2849
|
5. **Get Application URL**: Navigate to the **Config** tab and copy the **Application URL**.
|
|
2463
|
-
6. **
|
|
2850
|
+
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)).
|
|
2464
2851
|
7. **Next Steps**: See the [deploying-to-harper-fabric](deploying-to-harper-fabric.md) rule for detailed instructions on deploying your application successfully.
|
|
2465
2852
|
|
|
2466
2853
|
#### Examples
|
|
@@ -2468,12 +2855,15 @@ Follow these steps to set up your Harper Fabric environment for deployment.
|
|
|
2468
2855
|
##### Environment Configuration
|
|
2469
2856
|
|
|
2470
2857
|
```bash
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2858
|
+
harper login YOUR_CLUSTER_URL
|
|
2859
|
+
```
|
|
2860
|
+
|
|
2861
|
+
```bash
|
|
2862
|
+
# .env
|
|
2863
|
+
HARPER_CLI_TARGET='YOUR_CLUSTER_URL'
|
|
2474
2864
|
```
|
|
2475
2865
|
|
|
2476
|
-
### 4.
|
|
2866
|
+
### 4.4 Creating Harper Applications
|
|
2477
2867
|
|
|
2478
2868
|
The fastest way to start a new Harper project is using the `create-harper` CLI tool. This command
|
|
2479
2869
|
initializes a project with a standard folder structure, essential configuration files, and basic
|
|
@@ -2516,12 +2906,79 @@ npm create harper@latest my-app --template default
|
|
|
2516
2906
|
|
|
2517
2907
|
#### Next Steps
|
|
2518
2908
|
|
|
2519
|
-
1. **
|
|
2520
|
-
2. **
|
|
2521
|
-
3. **
|
|
2522
|
-
|
|
2909
|
+
1. **Define Schema**: Modify `schema.graphql` to fit your application's data model.
|
|
2910
|
+
2. **Start Development**: Run `npm run dev` to start the local Harper instance.
|
|
2911
|
+
3. **Connect the CLI to your cluster**: Create a cluster (see
|
|
2912
|
+
[creating-a-fabric-account-and-cluster](creating-a-fabric-account-and-cluster.md)), run `harper login` with its
|
|
2913
|
+
Application URL as a super user, and set that URL as `HARPER_CLI_TARGET` in the project's `.env` file. Don't put a
|
|
2914
|
+
cluster username or password in `.env` or in GitHub secrets.
|
|
2915
|
+
4. **Deploy**: See [Deploying](#deploying).
|
|
2916
|
+
|
|
2917
|
+
#### Deploying
|
|
2918
|
+
|
|
2919
|
+
Every template except the legacy `early-hints` example ships a GitHub Actions workflow,
|
|
2920
|
+
`.github/workflows/deploy.yaml`. It tests every pull request and deploys to the cluster when a change merges to `main`.
|
|
2921
|
+
The deploy job holds no Harper credential: its `harper` commands authenticate with GitHub's OIDC identity token under a
|
|
2922
|
+
trust policy on the cluster. The Next.js templates' workflow also runs `next build` before it uploads.
|
|
2923
|
+
|
|
2924
|
+
1. **Set up CI deploys once**: Push the project to a GitHub repository, then run `npm run deploy:setup-ci` from the
|
|
2925
|
+
project, logged in to the cluster as a super user. It runs `harper deploy setup=true provider=github-actions` for the
|
|
2926
|
+
project, which creates a deploy-only role and user and a trust policy matched to the workflow, and sets the
|
|
2927
|
+
repository's `HARPER_CLI_TARGET` variable when the `gh` CLI is signed in (otherwise it prints the command to run).
|
|
2928
|
+
Running it again changes nothing. See [deploying-from-ci](deploying-from-ci.md) for what it checks and how to
|
|
2929
|
+
customize it.
|
|
2930
|
+
|
|
2931
|
+
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
|
|
2932
|
+
up the same role, user, policy and variable by hand, as a super user. The policy must match the scaffolded workflow:
|
|
2933
|
+
its `deploy.yaml` file on `main`, in the `production` environment. The `audience` is the cluster URL with its port
|
|
2934
|
+
and a trailing slash, and `HARPER_CLI_TARGET` must be that same string:
|
|
2523
2935
|
|
|
2524
|
-
|
|
2936
|
+
```bash
|
|
2937
|
+
gh api repos/<owner>/<repo> --jq .id # the repository_id
|
|
2938
|
+
harper add_role role=<name>-ci-deploy permission='{"operations":["deploy_component","get_job","registration_info"]}'
|
|
2939
|
+
harper add_user username=<name>-ci-deploy role=<name>-ci-deploy active=true password="$(openssl rand -base64 32)"
|
|
2940
|
+
harper add_oidc_trust \
|
|
2941
|
+
id=github-actions-<name> \
|
|
2942
|
+
issuer=https://token.actions.githubusercontent.com \
|
|
2943
|
+
audience=https://my-cluster.example.com:9925/ \
|
|
2944
|
+
user=<name>-ci-deploy \
|
|
2945
|
+
claims='{"repository_id":"<id>","workflow_ref":"<owner>/<repo>/.github/workflows/deploy.yaml@refs/heads/main","environment":"production"}'
|
|
2946
|
+
gh variable set HARPER_CLI_TARGET --repo <owner>/<repo> --body https://my-cluster.example.com:9925/
|
|
2947
|
+
```
|
|
2948
|
+
|
|
2949
|
+
If you restrict the `production` environment's deployment branches, allow `main`.
|
|
2950
|
+
|
|
2951
|
+
2. **Deploy by merging to `main`**, or by running the workflow from the **Actions** tab. The workflow deploys with
|
|
2952
|
+
`restart=rolling` and waits for every node to take the release. The run's summary names the `deployment_id` and its
|
|
2953
|
+
`certification`.
|
|
2954
|
+
3. **Deploy by hand with `npm run deploy`**: It runs `harper deploy project=<name> restart=rolling` from your machine, as
|
|
2955
|
+
the user you logged in with.
|
|
2956
|
+
4. **Go back to an earlier release** by activating its `deployment_id`, with nothing reinstalled:
|
|
2957
|
+
|
|
2958
|
+
```bash
|
|
2959
|
+
harper list_deployments project=<name>
|
|
2960
|
+
harper deploy project=<name> deployment_id=<id> restart=rolling
|
|
2961
|
+
```
|
|
2962
|
+
|
|
2963
|
+
See [deploying-to-harper-fabric](deploying-to-harper-fabric.md).
|
|
2964
|
+
|
|
2965
|
+
Use your package manager's equivalent of `npm run` (`pnpm run`, `bun run`, `yarn`) if the project uses another one.
|
|
2966
|
+
|
|
2967
|
+
Projects created before create-harper 1.13.0 deploy on version tags with a `HARPER_CLI_REFRESH_TOKEN` secret, and
|
|
2968
|
+
don't have `deploy:setup-ci`. To move one over:
|
|
2969
|
+
|
|
2970
|
+
1. Scaffold a new project with the same name and template, and copy its `.github/workflows/deploy.yaml` over the old
|
|
2971
|
+
workflow.
|
|
2972
|
+
2. Set up the cluster for it, with the `project=` value that workflow passes to `harper deploy` as `<name>`: with the
|
|
2973
|
+
5.4.0 CLI, `harper deploy setup=true provider=github-actions project=<name>`; with an earlier one, by hand as in
|
|
2974
|
+
step 1.
|
|
2975
|
+
3. Push the new workflow to `main`.
|
|
2976
|
+
4. Once the workflow's run on `main` has succeeded, wait step included, delete the `HARPER_CLI_REFRESH_TOKEN` secret.
|
|
2977
|
+
Deleting it doesn't revoke the token, which stays valid until it expires. To cut it off now, mint a new one for that
|
|
2978
|
+
user with `harper login --for-ci`, which invalidates the old one, or deactivate the user with `alter_user` if
|
|
2979
|
+
nothing else uses it.
|
|
2980
|
+
|
|
2981
|
+
### 4.5 Serving Web Content
|
|
2525
2982
|
|
|
2526
2983
|
Instructions for the agent to follow when serving web content from Harper.
|
|
2527
2984
|
|
|
@@ -2628,7 +3085,7 @@ Because `@harperfast/vite` builds on the node and `static` serves the output, de
|
|
|
2628
3085
|
|
|
2629
3086
|
On deploy the plugin runs `vite build` at startup (and rebuilds when `files` change) while `static` serves the result. If you prefer to build in CI, commit the build output, point `static` at it, and omit `files` so the plugin stays idle while `static` serves the prebuilt assets. Either way, `npm create harper@latest` scaffolds a working setup for you.
|
|
2630
3087
|
|
|
2631
|
-
### 4.
|
|
3088
|
+
### 4.6 Harper Logging
|
|
2632
3089
|
|
|
2633
3090
|
Instructions for the agent to follow when implementing logging in Harper applications, including direct logger usage, tagged loggers, and console capture behavior.
|
|
2634
3091
|
|
|
@@ -2784,61 +3241,31 @@ Tagged entries appear in `hdb.log` with the tag in the header:
|
|
|
2784
3241
|
- When logging to standard streams, run Harper in the foreground (`harper`, not `harper start`).
|
|
2785
3242
|
- `TaggedLogger` is bound to the configured log level at creation time — always use `?.` on its methods.
|
|
2786
3243
|
|
|
2787
|
-
### 4.
|
|
3244
|
+
### 4.7 Load Environment Variables with loadEnv
|
|
2788
3245
|
|
|
2789
3246
|
Instructions for the agent to follow when loading environment variables from `.env` files into a Harper application using the `loadEnv` plugin.
|
|
2790
3247
|
|
|
2791
3248
|
#### When to Use
|
|
2792
3249
|
|
|
2793
|
-
Apply this rule when a Harper application needs to supply secrets, API endpoints, or other configuration values to component code via `process.env` without hardcoding them. Use
|
|
3250
|
+
Apply this rule when a Harper application needs to supply secrets, API endpoints, or other configuration values to component code via `process.env` without hardcoding them. Use this whenever a `.env` file must be loaded at application startup.
|
|
2794
3251
|
|
|
2795
3252
|
#### How It Works
|
|
2796
3253
|
|
|
2797
|
-
1. **Declare `loadEnv` in `config.yaml`**:
|
|
2798
|
-
|
|
2799
|
-
```yaml
|
|
2800
|
-
loadEnv:
|
|
2801
|
-
files: '.env'
|
|
2802
|
-
```
|
|
2803
|
-
|
|
2804
|
-
2. **Place `loadEnv` first**: Harper is a single-process application. List `loadEnv` before all other components so that environment variables are available on `process.env` before dependent components start.
|
|
3254
|
+
1. **Declare `loadEnv` first in `config.yaml`**: Place `loadEnv` before all other components so environment variables are available when dependent components start.
|
|
2805
3255
|
|
|
2806
3256
|
```yaml
|
|
2807
3257
|
# config.yaml — loadEnv must come first
|
|
2808
3258
|
loadEnv:
|
|
2809
3259
|
files: '.env'
|
|
2810
|
-
|
|
2811
|
-
rest: true
|
|
2812
|
-
|
|
2813
|
-
myApp:
|
|
2814
|
-
files: './src/*.js'
|
|
2815
3260
|
```
|
|
2816
3261
|
|
|
2817
|
-
|
|
3262
|
+
2. **Set the `files` option**: Provide one or more paths or glob patterns pointing to the env file(s) to load. This option is required.
|
|
2818
3263
|
|
|
2819
|
-
|
|
3264
|
+
3. **Access variables in component code**: After loading, all values from the specified files are available on `process.env` and shared across all components in the single Harper process.
|
|
2820
3265
|
|
|
2821
|
-
|
|
2822
|
-
loadEnv:
|
|
2823
|
-
files: '.env'
|
|
2824
|
-
override: true
|
|
2825
|
-
```
|
|
3266
|
+
4. **Control override behavior with `override`**: By default, existing environment variables take precedence over values in `.env` files. Set `override: true` to reverse this — loaded values will overwrite variables already present in the shell or container environment.
|
|
2826
3267
|
|
|
2827
|
-
5. **Load multiple files
|
|
2828
|
-
|
|
2829
|
-
```yaml
|
|
2830
|
-
loadEnv:
|
|
2831
|
-
files:
|
|
2832
|
-
- '.env'
|
|
2833
|
-
- '.env.local'
|
|
2834
|
-
```
|
|
2835
|
-
|
|
2836
|
-
or with a glob:
|
|
2837
|
-
|
|
2838
|
-
```yaml
|
|
2839
|
-
loadEnv:
|
|
2840
|
-
files: 'env-vars/*'
|
|
2841
|
-
```
|
|
3268
|
+
5. **Load multiple files when needed**: Specify a list or a glob pattern under `files`. Files are loaded in the order specified.
|
|
2842
3269
|
|
|
2843
3270
|
##### Configuration Options
|
|
2844
3271
|
|
|
@@ -2849,10 +3276,17 @@ Apply this rule when a Harper application needs to supply secrets, API endpoints
|
|
|
2849
3276
|
|
|
2850
3277
|
#### Examples
|
|
2851
3278
|
|
|
2852
|
-
**
|
|
3279
|
+
**Basic usage — load a single `.env` file:**
|
|
2853
3280
|
|
|
2854
3281
|
```yaml
|
|
2855
|
-
|
|
3282
|
+
loadEnv:
|
|
3283
|
+
files: '.env'
|
|
3284
|
+
```
|
|
3285
|
+
|
|
3286
|
+
**Full `config.yaml` with `loadEnv` declared first:**
|
|
3287
|
+
|
|
3288
|
+
```yaml
|
|
3289
|
+
# config.yaml — loadEnv must come first
|
|
2856
3290
|
loadEnv:
|
|
2857
3291
|
files: '.env'
|
|
2858
3292
|
|
|
@@ -2862,29 +3296,40 @@ myApp:
|
|
|
2862
3296
|
files: './src/*.js'
|
|
2863
3297
|
```
|
|
2864
3298
|
|
|
2865
|
-
**
|
|
3299
|
+
**Override existing environment variables:**
|
|
3300
|
+
|
|
3301
|
+
```yaml
|
|
3302
|
+
loadEnv:
|
|
3303
|
+
files: '.env'
|
|
3304
|
+
override: true
|
|
3305
|
+
```
|
|
3306
|
+
|
|
3307
|
+
**Load multiple files as a list:**
|
|
2866
3308
|
|
|
2867
3309
|
```yaml
|
|
2868
|
-
# config.yaml
|
|
2869
3310
|
loadEnv:
|
|
2870
3311
|
files:
|
|
2871
3312
|
- '.env'
|
|
2872
3313
|
- '.env.local'
|
|
2873
|
-
|
|
3314
|
+
```
|
|
2874
3315
|
|
|
2875
|
-
|
|
3316
|
+
**Load multiple files using a glob pattern:**
|
|
2876
3317
|
|
|
2877
|
-
|
|
2878
|
-
|
|
3318
|
+
```yaml
|
|
3319
|
+
loadEnv:
|
|
3320
|
+
files: 'env-vars/*'
|
|
2879
3321
|
```
|
|
2880
3322
|
|
|
2881
3323
|
#### Notes
|
|
2882
3324
|
|
|
2883
|
-
- `loadEnv`
|
|
2884
|
-
-
|
|
2885
|
-
-
|
|
3325
|
+
- `loadEnv` is built into Harper — it does not need to be installed, only declared in `config.yaml`.
|
|
3326
|
+
- Variables are loaded onto `process.env` and shared across all components. Declare `loadEnv` before any component that depends on those variables.
|
|
3327
|
+
- `loadEnv` supplies **application** environment variables only. It does **not** configure Harper itself. Harper's own configuration is composed at startup before any `loadEnv` runs, so variables such as `HARPER_CONFIG`, `HARPER_SET_CONFIG`, and `HARPER_DEFAULT_CONFIG` delivered through a `.env` file are read too late and are ignored.
|
|
3328
|
+
- To change Harper's own configuration, use the configuration file directly or export variables in the real process/container environment before Harper starts.
|
|
3329
|
+
- For production credentials, prefer the encrypted secrets store over a committed `.env` file. Secrets are delivered to components via `process.env` and never appear in operation logs or replication payloads.
|
|
3330
|
+
- Individual `.env` values can also be stored encrypted so plaintext never reaches the API, logs, or replication payload.
|
|
2886
3331
|
|
|
2887
|
-
### 4.
|
|
3332
|
+
### 4.8 v5 Upgrade: Breaking Changes and Migration Guide
|
|
2888
3333
|
|
|
2889
3334
|
Instructions for the agent to follow when migrating a Harper application to v5, covering all breaking changes and required code updates.
|
|
2890
3335
|
|
|
@@ -3056,7 +3501,7 @@ record = { ...record, property: 'changed' };
|
|
|
3056
3501
|
- Automatic context tracking simplifies code but requires explicit `commit()` or new `transaction()` calls when you need to observe data written within the same transaction.
|
|
3057
3502
|
- Use `getContext` from `'harper'` rather than passing context manually through every call — this is the recommended pattern in v5.
|
|
3058
3503
|
|
|
3059
|
-
### 4.
|
|
3504
|
+
### 4.9 Delegating to the Built-in Agent
|
|
3060
3505
|
|
|
3061
3506
|
Harper 5.2+ ships with a **built-in agent** that runs _inside_ the server, on the main thread
|
|
3062
3507
|
adjacent to the operations API. Because it runs in-process, it can do things a remote client
|