@azx-pbc/helix-cli 0.4.0 → 0.5.0
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/README.md +37 -3
- package/dist/helix.js +784 -174
- package/package.json +3 -3
package/dist/helix.js
CHANGED
|
@@ -13,6 +13,10 @@ var options = {
|
|
|
13
13
|
description: { type: "string" },
|
|
14
14
|
visibility: { type: "string" },
|
|
15
15
|
path: { type: "string" },
|
|
16
|
+
repo: { type: "string" },
|
|
17
|
+
branch: { type: "string" },
|
|
18
|
+
reusable: { type: "boolean" },
|
|
19
|
+
force: { type: "boolean" },
|
|
16
20
|
help: { type: "boolean" }
|
|
17
21
|
};
|
|
18
22
|
function parseCliArgs(argv) {
|
|
@@ -21,7 +25,7 @@ function parseCliArgs(argv) {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
// src/client.ts
|
|
24
|
-
import { z as
|
|
28
|
+
import { z as z29 } from "zod";
|
|
25
29
|
|
|
26
30
|
// ../shared/src/visibility.ts
|
|
27
31
|
import { z } from "zod";
|
|
@@ -176,6 +180,16 @@ var DeployReportSchema = z4.object({
|
|
|
176
180
|
/** Candidate roots the planner considered. */
|
|
177
181
|
candidates: z4.array(z4.string().max(1024)).max(20).default([])
|
|
178
182
|
});
|
|
183
|
+
var VersionSourceSchema = z4.object({
|
|
184
|
+
provider: z4.literal("github"),
|
|
185
|
+
repoFullName: z4.string(),
|
|
186
|
+
repositoryId: z4.string(),
|
|
187
|
+
ref: z4.string(),
|
|
188
|
+
commitSha: z4.string(),
|
|
189
|
+
workflowRef: z4.string(),
|
|
190
|
+
runId: z4.string(),
|
|
191
|
+
runAttempt: z4.int().positive()
|
|
192
|
+
});
|
|
179
193
|
var VersionSchema = z4.object({
|
|
180
194
|
id: z4.uuid(),
|
|
181
195
|
appId: z4.uuid(),
|
|
@@ -186,7 +200,9 @@ var VersionSchema = z4.object({
|
|
|
186
200
|
status: VersionStatusSchema,
|
|
187
201
|
createdAt: z4.iso.datetime(),
|
|
188
202
|
/** Client-asserted salvage provenance (ADR-0038); absent on CLI uploads. */
|
|
189
|
-
deployReport: DeployReportSchema.optional()
|
|
203
|
+
deployReport: DeployReportSchema.optional(),
|
|
204
|
+
/** Verified CI provenance (ADR-0051); absent on non-CI uploads. */
|
|
205
|
+
source: VersionSourceSchema.optional()
|
|
190
206
|
});
|
|
191
207
|
|
|
192
208
|
// ../shared/src/manifest.ts
|
|
@@ -1256,7 +1272,20 @@ var DeploymentConfigResponseSchema = z13.object({
|
|
|
1256
1272
|
* chasing a rejection it can't see the cause of.
|
|
1257
1273
|
*/
|
|
1258
1274
|
deployMaxFileMb: z13.number().positive().optional(),
|
|
1259
|
-
deployMaxBundleMb: z13.number().positive().optional()
|
|
1275
|
+
deployMaxBundleMb: z13.number().positive().optional(),
|
|
1276
|
+
/**
|
|
1277
|
+
* Repo-backed deploys (ADR-0051). `audience` is the exact `aud` the CI route
|
|
1278
|
+
* accepts, so `helix deploy` in GitHub Actions requests its token for this
|
|
1279
|
+
* value rather than for whatever URL it was pointed at. `serverUrl` is the
|
|
1280
|
+
* GitHub web origin for commit and run links. Optional only to tolerate a
|
|
1281
|
+
* portal that predates the feature; when `enabled` is false the CI route and
|
|
1282
|
+
* the binding routes answer 404 and clients hide the surface.
|
|
1283
|
+
*/
|
|
1284
|
+
repoDeploy: z13.object({
|
|
1285
|
+
enabled: z13.boolean(),
|
|
1286
|
+
audience: z13.url(),
|
|
1287
|
+
serverUrl: z13.url()
|
|
1288
|
+
}).optional()
|
|
1260
1289
|
});
|
|
1261
1290
|
|
|
1262
1291
|
// ../shared/src/catalogue.ts
|
|
@@ -1718,7 +1747,12 @@ var GatewayCallSchema = z18.object({
|
|
|
1718
1747
|
method: z18.string().nullable(),
|
|
1719
1748
|
/** LLM stop reason; null for non-LLM calls. */
|
|
1720
1749
|
stopReason: z18.string().nullable(),
|
|
1721
|
-
/**
|
|
1750
|
+
/**
|
|
1751
|
+
* Short upstream error string; null on success. Admin-only in practice — it
|
|
1752
|
+
* can quote request content and, on an auth failure, the key, so it serves
|
|
1753
|
+
* the admin audit log's audience while the app-scoped feed
|
|
1754
|
+
* (`/api/v1/apps/:slug/audit`) nulls it for non-admin callers.
|
|
1755
|
+
*/
|
|
1722
1756
|
errorDetail: z18.string().nullable(),
|
|
1723
1757
|
outcome: GatewayOutcomeSchema,
|
|
1724
1758
|
createdAt: z18.iso.datetime()
|
|
@@ -1763,11 +1797,59 @@ var PlatformUsageSchema = z18.object({
|
|
|
1763
1797
|
)
|
|
1764
1798
|
});
|
|
1765
1799
|
|
|
1766
|
-
// ../shared/src/
|
|
1800
|
+
// ../shared/src/visitors.ts
|
|
1767
1801
|
import { z as z19 } from "zod";
|
|
1768
|
-
var
|
|
1769
|
-
|
|
1770
|
-
|
|
1802
|
+
var VISITOR_RANGES = ["7d", "30d", "90d"];
|
|
1803
|
+
var VisitorRangeSchema = z19.enum(VISITOR_RANGES);
|
|
1804
|
+
var count = z19.int().nonnegative();
|
|
1805
|
+
var VisitorTotalsSchema = z19.object({
|
|
1806
|
+
visits: count,
|
|
1807
|
+
uniqueVisitors: count
|
|
1808
|
+
});
|
|
1809
|
+
var GeoStatusSchema = z19.object({
|
|
1810
|
+
available: z19.boolean(),
|
|
1811
|
+
reason: z19.string().nullable(),
|
|
1812
|
+
attribution: z19.object({ text: z19.string(), url: z19.url() }).nullable()
|
|
1813
|
+
});
|
|
1814
|
+
var VisitorLocationSchema = z19.object({
|
|
1815
|
+
/** ISO 3166-1 alpha-2. */
|
|
1816
|
+
country: z19.string(),
|
|
1817
|
+
countryName: z19.string().nullable(),
|
|
1818
|
+
/** First-level subdivision (state, province); null when the database has none. */
|
|
1819
|
+
region: z19.string().nullable(),
|
|
1820
|
+
visitors: count,
|
|
1821
|
+
visits: count
|
|
1822
|
+
});
|
|
1823
|
+
var VisitorSeriesPointSchema = z19.object({
|
|
1824
|
+
/** Bucket-start ISO timestamp (one calendar day). */
|
|
1825
|
+
bucket: z19.iso.datetime(),
|
|
1826
|
+
visits: count,
|
|
1827
|
+
/** Distinct visitors that day — a visitor active on two days counts on each. */
|
|
1828
|
+
uniqueVisitors: count
|
|
1829
|
+
});
|
|
1830
|
+
var VisitorSummarySchema = z19.object({
|
|
1831
|
+
appId: z19.uuid(),
|
|
1832
|
+
range: VisitorRangeSchema,
|
|
1833
|
+
/** The selected window. */
|
|
1834
|
+
current: VisitorTotalsSchema,
|
|
1835
|
+
/** The window of the same length immediately before it, for the deltas. */
|
|
1836
|
+
prior: VisitorTotalsSchema,
|
|
1837
|
+
/** Dense, zero-filled daily buckets, oldest first. */
|
|
1838
|
+
series: z19.array(VisitorSeriesPointSchema),
|
|
1839
|
+
/** The top {@link VISITOR_LOCATION_LIMIT} locations by visitors. */
|
|
1840
|
+
locations: z19.array(VisitorLocationSchema),
|
|
1841
|
+
/** Every resolved location past the top ones. */
|
|
1842
|
+
otherLocations: VisitorTotalsSchema,
|
|
1843
|
+
/** Private, proxied or unknown networks, and every row when geo is unavailable. */
|
|
1844
|
+
unresolved: VisitorTotalsSchema,
|
|
1845
|
+
geo: GeoStatusSchema
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
// ../shared/src/data.ts
|
|
1849
|
+
import { z as z20 } from "zod";
|
|
1850
|
+
var CollectionItemSchema = z20.object({
|
|
1851
|
+
id: z20.uuid(),
|
|
1852
|
+
collection: z20.string(),
|
|
1771
1853
|
/**
|
|
1772
1854
|
* Which tier collected the row (dev-mode §5). The runtime roles are RLS-pinned
|
|
1773
1855
|
* to one tier each, but the portal reads across both, so the discriminator has
|
|
@@ -1776,141 +1858,141 @@ var CollectionItemSchema = z19.object({
|
|
|
1776
1858
|
*/
|
|
1777
1859
|
env: EnvSchema,
|
|
1778
1860
|
/** The submitting user, if authenticated; null for anonymous/public visitors. */
|
|
1779
|
-
userOid:
|
|
1861
|
+
userOid: z20.string().nullable(),
|
|
1780
1862
|
/**
|
|
1781
1863
|
* The submitter's captured claims — display half, rendered but never compared
|
|
1782
1864
|
* (see `GatewayCallSchema.userName`). Null whenever `userOid` is, and for
|
|
1783
1865
|
* password/dev-token principals.
|
|
1784
1866
|
*/
|
|
1785
|
-
userName:
|
|
1786
|
-
userEmail:
|
|
1867
|
+
userName: z20.string().nullable(),
|
|
1868
|
+
userEmail: z20.string().nullable(),
|
|
1787
1869
|
/** The submitter's kind; null whenever `userOid` is, and on pre-column rows. */
|
|
1788
1870
|
userKind: PrincipalKindSchema.nullable(),
|
|
1789
|
-
item:
|
|
1871
|
+
item: z20.unknown(),
|
|
1790
1872
|
/** Hashed IP / truncated UA for abuse triage; null if not captured. */
|
|
1791
|
-
meta:
|
|
1792
|
-
createdAt:
|
|
1873
|
+
meta: z20.unknown().nullable(),
|
|
1874
|
+
createdAt: z20.iso.datetime()
|
|
1793
1875
|
});
|
|
1794
|
-
var CollectionItemsPageSchema =
|
|
1795
|
-
rows:
|
|
1876
|
+
var CollectionItemsPageSchema = z20.object({
|
|
1877
|
+
rows: z20.array(CollectionItemSchema),
|
|
1796
1878
|
/** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
|
|
1797
|
-
nextBefore:
|
|
1879
|
+
nextBefore: z20.iso.datetime().optional()
|
|
1798
1880
|
});
|
|
1799
|
-
var CollectionSummarySchema =
|
|
1800
|
-
name:
|
|
1881
|
+
var CollectionSummarySchema = z20.object({
|
|
1882
|
+
name: z20.string(),
|
|
1801
1883
|
env: EnvSchema,
|
|
1802
|
-
count:
|
|
1884
|
+
count: z20.int().nonnegative(),
|
|
1803
1885
|
/** Newest row's timestamp; null only if the group is somehow empty. */
|
|
1804
|
-
lastAt:
|
|
1886
|
+
lastAt: z20.iso.datetime().nullable()
|
|
1805
1887
|
});
|
|
1806
1888
|
|
|
1807
1889
|
// ../shared/src/collectionTable.ts
|
|
1808
1890
|
var BOM = String.fromCharCode(65279);
|
|
1809
1891
|
|
|
1810
1892
|
// ../shared/src/connections.ts
|
|
1811
|
-
import { z as
|
|
1893
|
+
import { z as z21 } from "zod";
|
|
1812
1894
|
var CONNECTION_STATUSES = ["live", "reconnect-needed", "invalidated"];
|
|
1813
|
-
var ConnectionStatusSchema =
|
|
1814
|
-
var ConnectionMaterialSchema =
|
|
1815
|
-
access:
|
|
1816
|
-
refresh:
|
|
1895
|
+
var ConnectionStatusSchema = z21.enum(CONNECTION_STATUSES);
|
|
1896
|
+
var ConnectionMaterialSchema = z21.strictObject({
|
|
1897
|
+
access: z21.string().min(1),
|
|
1898
|
+
refresh: z21.string().min(1)
|
|
1817
1899
|
});
|
|
1818
1900
|
var MAX_GRANTED_SCOPES = 64;
|
|
1819
|
-
var GrantedScopesSchema =
|
|
1820
|
-
var UserConnectionSchema =
|
|
1821
|
-
id:
|
|
1822
|
-
userOid:
|
|
1823
|
-
providerId:
|
|
1824
|
-
providerRevision:
|
|
1901
|
+
var GrantedScopesSchema = z21.array(ScopeTokenSchema).max(MAX_GRANTED_SCOPES);
|
|
1902
|
+
var UserConnectionSchema = z21.strictObject({
|
|
1903
|
+
id: z21.uuid(),
|
|
1904
|
+
userOid: z21.string().min(1),
|
|
1905
|
+
providerId: z21.uuid(),
|
|
1906
|
+
providerRevision: z21.int().positive(),
|
|
1825
1907
|
env: EnvSchema,
|
|
1826
1908
|
status: ConnectionStatusSchema,
|
|
1827
|
-
material:
|
|
1909
|
+
material: z21.string().min(1),
|
|
1828
1910
|
grantedScopes: GrantedScopesSchema,
|
|
1829
|
-
grantedAt:
|
|
1830
|
-
expiresAt:
|
|
1831
|
-
renewBeforeNext:
|
|
1832
|
-
pendingRetire:
|
|
1833
|
-
lastRenewedAt:
|
|
1834
|
-
createdAt:
|
|
1835
|
-
updatedAt:
|
|
1836
|
-
});
|
|
1837
|
-
var MyConnectionSchema =
|
|
1838
|
-
id:
|
|
1911
|
+
grantedAt: z21.iso.datetime(),
|
|
1912
|
+
expiresAt: z21.iso.datetime(),
|
|
1913
|
+
renewBeforeNext: z21.boolean(),
|
|
1914
|
+
pendingRetire: z21.string().min(1).nullable(),
|
|
1915
|
+
lastRenewedAt: z21.iso.datetime().nullable(),
|
|
1916
|
+
createdAt: z21.iso.datetime(),
|
|
1917
|
+
updatedAt: z21.iso.datetime()
|
|
1918
|
+
});
|
|
1919
|
+
var MyConnectionSchema = z21.strictObject({
|
|
1920
|
+
id: z21.uuid(),
|
|
1839
1921
|
providerRef: ProviderRefSchema,
|
|
1840
|
-
providerDisplayName:
|
|
1922
|
+
providerDisplayName: z21.string().min(1).max(200),
|
|
1841
1923
|
env: EnvSchema,
|
|
1842
1924
|
status: ConnectionStatusSchema.exclude(["invalidated"]),
|
|
1843
1925
|
grantedScopes: GrantedScopesSchema,
|
|
1844
|
-
grantedAt:
|
|
1845
|
-
sharedApps:
|
|
1926
|
+
grantedAt: z21.iso.datetime(),
|
|
1927
|
+
sharedApps: z21.array(BoundAppSchema)
|
|
1846
1928
|
});
|
|
1847
|
-
var MyConnectionsResponseSchema =
|
|
1848
|
-
connections:
|
|
1929
|
+
var MyConnectionsResponseSchema = z21.strictObject({
|
|
1930
|
+
connections: z21.array(MyConnectionSchema)
|
|
1849
1931
|
});
|
|
1850
|
-
var DisconnectResponseSchema =
|
|
1851
|
-
outcome:
|
|
1932
|
+
var DisconnectResponseSchema = z21.strictObject({
|
|
1933
|
+
outcome: z21.enum(["disconnected", "already_removed"])
|
|
1852
1934
|
});
|
|
1853
1935
|
|
|
1854
1936
|
// ../shared/src/consent.ts
|
|
1855
|
-
import { z as
|
|
1856
|
-
var ConsentStateSchema =
|
|
1857
|
-
var ConsentNonceSchema =
|
|
1858
|
-
var ConsentAttemptTagSchema =
|
|
1859
|
-
var ConsentIdentitySchema =
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
kind:
|
|
1863
|
-
developerOid:
|
|
1937
|
+
import { z as z22 } from "zod";
|
|
1938
|
+
var ConsentStateSchema = z22.string().min(43).max(128);
|
|
1939
|
+
var ConsentNonceSchema = z22.string().min(16).max(128);
|
|
1940
|
+
var ConsentAttemptTagSchema = z22.string().min(1).max(128).regex(/^[A-Za-z0-9._~-]+$/, "must be URL-safe punctuation, letters, or digits");
|
|
1941
|
+
var ConsentIdentitySchema = z22.discriminatedUnion("kind", [
|
|
1942
|
+
z22.strictObject({ kind: z22.literal("user"), userOid: z22.string().min(1).max(128) }),
|
|
1943
|
+
z22.strictObject({
|
|
1944
|
+
kind: z22.literal("dev"),
|
|
1945
|
+
developerOid: z22.string().min(1).max(128),
|
|
1864
1946
|
nonce: ConsentNonceSchema
|
|
1865
1947
|
})
|
|
1866
1948
|
]);
|
|
1867
1949
|
var HttpUrlNoUserinfoSchema = (() => {
|
|
1868
1950
|
const userinfo = /^https?:\/\/[/\\]*[^/?#\\]*@/i;
|
|
1869
|
-
return
|
|
1951
|
+
return z22.url({ protocol: /^https?$/ }).refine(
|
|
1870
1952
|
(value) => !userinfo.test(value.replace(/[\t\n\r]/g, "")),
|
|
1871
1953
|
"URL must not carry userinfo"
|
|
1872
1954
|
);
|
|
1873
1955
|
})();
|
|
1874
|
-
var ConsultRequestSchema =
|
|
1956
|
+
var ConsultRequestSchema = z22.strictObject({
|
|
1875
1957
|
identity: ConsentIdentitySchema,
|
|
1876
|
-
appSlug:
|
|
1958
|
+
appSlug: z22.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
1877
1959
|
providerRef: ProviderRefSchema,
|
|
1878
1960
|
openerOrigin: HttpUrlNoUserinfoSchema.transform((value) => new URL(value).origin),
|
|
1879
1961
|
callbackUrl: HttpUrlNoUserinfoSchema
|
|
1880
1962
|
});
|
|
1881
|
-
var ConsultResponseSchema =
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1963
|
+
var ConsultResponseSchema = z22.discriminatedUnion("outcome", [
|
|
1964
|
+
z22.strictObject({ outcome: z22.literal("already_connected") }),
|
|
1965
|
+
z22.strictObject({ outcome: z22.literal("not_available") }),
|
|
1966
|
+
z22.strictObject({ outcome: z22.literal("started"), authorizeUrl: z22.url() })
|
|
1885
1967
|
]);
|
|
1886
|
-
var CancelRequestSchema =
|
|
1968
|
+
var CancelRequestSchema = z22.strictObject({
|
|
1887
1969
|
identity: ConsentIdentitySchema,
|
|
1888
1970
|
state: ConsentStateSchema
|
|
1889
1971
|
});
|
|
1890
1972
|
var CONSENT_CANCEL_OUTCOMES = ["cancelled", "not_cancellable"];
|
|
1891
|
-
var CancelResponseSchema =
|
|
1892
|
-
outcome:
|
|
1973
|
+
var CancelResponseSchema = z22.strictObject({
|
|
1974
|
+
outcome: z22.enum(CONSENT_CANCEL_OUTCOMES)
|
|
1893
1975
|
});
|
|
1894
|
-
var ConnectionStatusIdentitySchema =
|
|
1895
|
-
|
|
1896
|
-
|
|
1976
|
+
var ConnectionStatusIdentitySchema = z22.discriminatedUnion("kind", [
|
|
1977
|
+
z22.strictObject({ kind: z22.literal("user"), userOid: z22.string().min(1).max(128) }),
|
|
1978
|
+
z22.strictObject({ kind: z22.literal("dev"), developerOid: z22.string().min(1).max(128) })
|
|
1897
1979
|
]);
|
|
1898
|
-
var ConnectionStatusRequestSchema =
|
|
1980
|
+
var ConnectionStatusRequestSchema = z22.strictObject({
|
|
1899
1981
|
identity: ConnectionStatusIdentitySchema,
|
|
1900
|
-
appSlug:
|
|
1982
|
+
appSlug: z22.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
1901
1983
|
providerRef: ProviderRefSchema
|
|
1902
1984
|
});
|
|
1903
|
-
var ConnectionStatusResponseSchema =
|
|
1904
|
-
status:
|
|
1985
|
+
var ConnectionStatusResponseSchema = z22.strictObject({
|
|
1986
|
+
status: z22.enum(["connected", "not_connected", "not_available"])
|
|
1905
1987
|
});
|
|
1906
|
-
var AttemptCancelRequestSchema =
|
|
1988
|
+
var AttemptCancelRequestSchema = z22.strictObject({
|
|
1907
1989
|
provider: ProviderRefSchema,
|
|
1908
1990
|
attempt: ConsentAttemptTagSchema
|
|
1909
1991
|
});
|
|
1910
|
-
var DevConsentStartResponseSchema =
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1992
|
+
var DevConsentStartResponseSchema = z22.discriminatedUnion("outcome", [
|
|
1993
|
+
z22.strictObject({ outcome: z22.literal("started"), popupUrl: z22.url() }),
|
|
1994
|
+
z22.strictObject({ outcome: z22.literal("already_connected") }),
|
|
1995
|
+
z22.strictObject({ outcome: z22.literal("not_available") })
|
|
1914
1996
|
]);
|
|
1915
1997
|
var HELIX_CONNECT_MESSAGE_SOURCE = "helix-connect";
|
|
1916
1998
|
var CONNECT_MESSAGE_VERSION = 1;
|
|
@@ -1931,32 +2013,32 @@ var CONSENT_MESSAGE_REASONS = [
|
|
|
1931
2013
|
"provider_incompatible",
|
|
1932
2014
|
"service_unavailable"
|
|
1933
2015
|
];
|
|
1934
|
-
var ConnectOutcomeMessageSchema =
|
|
1935
|
-
source:
|
|
1936
|
-
version:
|
|
2016
|
+
var ConnectOutcomeMessageSchema = z22.strictObject({
|
|
2017
|
+
source: z22.literal(HELIX_CONNECT_MESSAGE_SOURCE),
|
|
2018
|
+
version: z22.literal(CONNECT_MESSAGE_VERSION),
|
|
1937
2019
|
attempt: ConsentAttemptTagSchema.optional(),
|
|
1938
2020
|
provider: ProviderRefSchema,
|
|
1939
|
-
outcome:
|
|
1940
|
-
reason:
|
|
2021
|
+
outcome: z22.enum(CONSENT_MESSAGE_OUTCOMES),
|
|
2022
|
+
reason: z22.enum(CONSENT_MESSAGE_REASONS).nullable()
|
|
1941
2023
|
}).refine((message) => message.outcome === "error" || message.reason === null, {
|
|
1942
2024
|
error: "reason is only carried with the error outcome"
|
|
1943
2025
|
});
|
|
1944
|
-
var ConnectResultSchema =
|
|
1945
|
-
outcome:
|
|
1946
|
-
provider:
|
|
2026
|
+
var ConnectResultSchema = z22.object({
|
|
2027
|
+
outcome: z22.enum(CONSENT_MESSAGE_OUTCOMES),
|
|
2028
|
+
provider: z22.string(),
|
|
1947
2029
|
attempt: ConsentAttemptTagSchema.optional(),
|
|
1948
|
-
reason:
|
|
2030
|
+
reason: z22.enum(CONSENT_MESSAGE_REASONS).optional()
|
|
1949
2031
|
}).refine((result) => result.outcome === "error" || result.reason === void 0, {
|
|
1950
2032
|
error: "reason is only carried with the error outcome"
|
|
1951
2033
|
});
|
|
1952
2034
|
|
|
1953
2035
|
// ../shared/src/exchange.ts
|
|
1954
|
-
import { z as
|
|
1955
|
-
var ExchangeCodeSchema =
|
|
1956
|
-
var CodeVerifierSchema =
|
|
1957
|
-
var ExchangeRequestSchema =
|
|
1958
|
-
providerId:
|
|
1959
|
-
providerRevision:
|
|
2036
|
+
import { z as z23 } from "zod";
|
|
2037
|
+
var ExchangeCodeSchema = z23.string().min(1).max(1024);
|
|
2038
|
+
var CodeVerifierSchema = z23.string().min(43).max(128).regex(/^[A-Za-z0-9\-._~]+$/, "must be an RFC 7636 code_verifier (43-128 unreserved characters)");
|
|
2039
|
+
var ExchangeRequestSchema = z23.strictObject({
|
|
2040
|
+
providerId: z23.uuid(),
|
|
2041
|
+
providerRevision: z23.int().positive(),
|
|
1960
2042
|
env: EnvSchema,
|
|
1961
2043
|
code: ExchangeCodeSchema,
|
|
1962
2044
|
codeVerifier: CodeVerifierSchema,
|
|
@@ -1967,24 +2049,24 @@ var EXCHANGE_REJECTION_REASONS = [
|
|
|
1967
2049
|
"missing_refresh_token",
|
|
1968
2050
|
"missing_permissions"
|
|
1969
2051
|
];
|
|
1970
|
-
var ExchangeRejectionReasonSchema =
|
|
2052
|
+
var ExchangeRejectionReasonSchema = z23.enum(EXCHANGE_REJECTION_REASONS);
|
|
1971
2053
|
var MAX_EXCHANGED_SCOPES = 64;
|
|
1972
|
-
var ExchangedScopesSchema =
|
|
1973
|
-
var ExchangeResponseSchema =
|
|
1974
|
-
|
|
1975
|
-
outcome:
|
|
1976
|
-
access:
|
|
1977
|
-
refresh:
|
|
2054
|
+
var ExchangedScopesSchema = z23.array(ScopeTokenSchema).max(MAX_EXCHANGED_SCOPES);
|
|
2055
|
+
var ExchangeResponseSchema = z23.discriminatedUnion("outcome", [
|
|
2056
|
+
z23.strictObject({
|
|
2057
|
+
outcome: z23.literal("exchanged"),
|
|
2058
|
+
access: z23.string().min(1),
|
|
2059
|
+
refresh: z23.string().min(1),
|
|
1978
2060
|
grantedScopes: ExchangedScopesSchema,
|
|
1979
|
-
accessExpiresAt:
|
|
2061
|
+
accessExpiresAt: z23.iso.datetime()
|
|
1980
2062
|
}),
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2063
|
+
z23.strictObject({ outcome: z23.literal("rejected"), reason: ExchangeRejectionReasonSchema }),
|
|
2064
|
+
z23.strictObject({ outcome: z23.literal("provider_unavailable") }),
|
|
2065
|
+
z23.strictObject({ outcome: z23.literal("exchange_failed") })
|
|
1984
2066
|
]);
|
|
1985
2067
|
|
|
1986
2068
|
// ../shared/src/devTokens.ts
|
|
1987
|
-
import { z as
|
|
2069
|
+
import { z as z24 } from "zod";
|
|
1988
2070
|
function isValidDevOrigin(input) {
|
|
1989
2071
|
if (input.includes("*")) return false;
|
|
1990
2072
|
let url;
|
|
@@ -1998,40 +2080,83 @@ function isValidDevOrigin(input) {
|
|
|
1998
2080
|
if (url.username !== "" || url.password !== "") return false;
|
|
1999
2081
|
return input === url.origin || input === `${url.origin}/`;
|
|
2000
2082
|
}
|
|
2001
|
-
var DevOriginSchema =
|
|
2083
|
+
var DevOriginSchema = z24.string().refine(
|
|
2002
2084
|
isValidDevOrigin,
|
|
2003
2085
|
"must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
|
|
2004
2086
|
).transform((o) => new URL(o).origin);
|
|
2005
|
-
var TtlDaysSchema =
|
|
2006
|
-
var DevTokenMintRequestSchema =
|
|
2007
|
-
origins:
|
|
2087
|
+
var TtlDaysSchema = z24.number().int().min(1).max(365).optional();
|
|
2088
|
+
var DevTokenMintRequestSchema = z24.object({
|
|
2089
|
+
origins: z24.array(DevOriginSchema).min(1).max(20),
|
|
2008
2090
|
ttlDays: TtlDaysSchema
|
|
2009
2091
|
});
|
|
2010
|
-
var DevTokenRotateRequestSchema =
|
|
2092
|
+
var DevTokenRotateRequestSchema = z24.object({
|
|
2011
2093
|
ttlDays: TtlDaysSchema
|
|
2012
2094
|
});
|
|
2013
|
-
var DevTokenMetadataSchema =
|
|
2014
|
-
id:
|
|
2015
|
-
developerOid:
|
|
2016
|
-
origins:
|
|
2017
|
-
expiresAt:
|
|
2018
|
-
revokedAt:
|
|
2019
|
-
createdAt:
|
|
2095
|
+
var DevTokenMetadataSchema = z24.object({
|
|
2096
|
+
id: z24.string(),
|
|
2097
|
+
developerOid: z24.string(),
|
|
2098
|
+
origins: z24.array(z24.string()),
|
|
2099
|
+
expiresAt: z24.string(),
|
|
2100
|
+
revokedAt: z24.string().nullable().optional(),
|
|
2101
|
+
createdAt: z24.string()
|
|
2020
2102
|
});
|
|
2021
|
-
var DevTokenMintResponseSchema =
|
|
2022
|
-
token:
|
|
2103
|
+
var DevTokenMintResponseSchema = z24.object({
|
|
2104
|
+
token: z24.string(),
|
|
2023
2105
|
metadata: DevTokenMetadataSchema
|
|
2024
2106
|
});
|
|
2025
2107
|
|
|
2108
|
+
// ../shared/src/repoBinding.ts
|
|
2109
|
+
import { z as z25 } from "zod";
|
|
2110
|
+
function hasControlChar(v) {
|
|
2111
|
+
for (let i = 0; i < v.length; i++) {
|
|
2112
|
+
const c = v.charCodeAt(i);
|
|
2113
|
+
if (c < 32 || c === 127) return true;
|
|
2114
|
+
}
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
var RepoFullNameSchema = z25.string().max(140).regex(
|
|
2118
|
+
/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/,
|
|
2119
|
+
"must be owner/name, as in the repository's GitHub URL"
|
|
2120
|
+
).refine((v) => !/\/\.{1,2}$/.test(v), "must be owner/name, as in the repository's GitHub URL");
|
|
2121
|
+
var BranchNameSchema = z25.string().min(1).max(255).refine(
|
|
2122
|
+
(v) => !v.startsWith("refs/") && !hasControlChar(v) && !/[\s~^:?*[\\]/.test(v) && !v.includes("..") && !v.includes("@{") && !v.includes("//") && !/^[-/]/.test(v) && !/(\/|\.lock|\.)$/.test(v),
|
|
2123
|
+
"must be a branch name such as main, without refs/heads/"
|
|
2124
|
+
);
|
|
2125
|
+
var WorkflowRefSchema = z25.string().max(512).regex(
|
|
2126
|
+
/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}\/\.github\/workflows\/[^@\s]+\.ya?ml@\S+$/,
|
|
2127
|
+
"must look like owner/repo/.github/workflows/deploy.yml@refs/heads/main"
|
|
2128
|
+
);
|
|
2129
|
+
var EnvironmentNameSchema = z25.string().min(1).max(255).refine((v) => !hasControlChar(v) && v.trim() === v, "must be an environment name");
|
|
2130
|
+
var SetRepoBindingRequestSchema = z25.object({
|
|
2131
|
+
repoFullName: RepoFullNameSchema,
|
|
2132
|
+
branch: BranchNameSchema,
|
|
2133
|
+
workflowRef: WorkflowRefSchema.optional(),
|
|
2134
|
+
environment: EnvironmentNameSchema.optional()
|
|
2135
|
+
});
|
|
2136
|
+
var REPO_PROVIDERS = ["github"];
|
|
2137
|
+
var RepoBindingSchema = z25.object({
|
|
2138
|
+
provider: z25.enum(REPO_PROVIDERS),
|
|
2139
|
+
repoFullName: z25.string(),
|
|
2140
|
+
branch: z25.string(),
|
|
2141
|
+
workflowRef: z25.string().optional(),
|
|
2142
|
+
environment: z25.string().optional(),
|
|
2143
|
+
/** Pinned from the first accepted token; absent until then. */
|
|
2144
|
+
repositoryId: z25.string().optional(),
|
|
2145
|
+
repositoryOwnerId: z25.string().optional(),
|
|
2146
|
+
pinnedAt: z25.iso.datetime().optional(),
|
|
2147
|
+
createdAt: z25.iso.datetime(),
|
|
2148
|
+
updatedAt: z25.iso.datetime()
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2026
2151
|
// ../shared/src/instruction.ts
|
|
2027
|
-
import { z as
|
|
2152
|
+
import { z as z26 } from "zod";
|
|
2028
2153
|
var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
|
|
2029
|
-
var InstructionCapabilitySchema =
|
|
2030
|
-
var AttestedInstructionSchema =
|
|
2154
|
+
var InstructionCapabilitySchema = z26.enum(INSTRUCTION_CAPABILITIES);
|
|
2155
|
+
var AttestedInstructionSchema = z26.strictObject({
|
|
2031
2156
|
/** App the call is attributed to (registry app id). */
|
|
2032
|
-
appId:
|
|
2157
|
+
appId: z26.string().min(1),
|
|
2033
2158
|
/** Authenticated user, or the anonymous sentinel on `public` apps. */
|
|
2034
|
-
userOid:
|
|
2159
|
+
userOid: z26.string().min(1),
|
|
2035
2160
|
/**
|
|
2036
2161
|
* Which kind of principal `userOid` is (the `PrincipalKind` vocabulary the
|
|
2037
2162
|
* edge records at capture time — recorded, never inferred). The delegated
|
|
@@ -2048,9 +2173,9 @@ var AttestedInstructionSchema = z24.strictObject({
|
|
|
2048
2173
|
userKind: PrincipalKindSchema.optional(),
|
|
2049
2174
|
capability: InstructionCapabilitySchema,
|
|
2050
2175
|
/** The allowlisted origin the edge authorized (scheme + host + port). */
|
|
2051
|
-
origin:
|
|
2176
|
+
origin: z26.url(),
|
|
2052
2177
|
/** Connection (secret) name to inject, if this is a secret-backed call. */
|
|
2053
|
-
connection:
|
|
2178
|
+
connection: z26.string().min(1).optional(),
|
|
2054
2179
|
/**
|
|
2055
2180
|
* Provider reference for a delegated call — the user's connection to this
|
|
2056
2181
|
* provider is what egress injects. Exactly
|
|
@@ -2060,7 +2185,7 @@ var AttestedInstructionSchema = z24.strictObject({
|
|
|
2060
2185
|
*/
|
|
2061
2186
|
provider: ProviderRefSchema.optional(),
|
|
2062
2187
|
/** Correlates the edge audit row with the egress call. */
|
|
2063
|
-
requestId:
|
|
2188
|
+
requestId: z26.string().min(1),
|
|
2064
2189
|
/**
|
|
2065
2190
|
* The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
|
|
2066
2191
|
* Egress refuses a mismatched verb/resource, so a captured instruction can't be
|
|
@@ -2075,8 +2200,8 @@ var AttestedInstructionSchema = z24.strictObject({
|
|
|
2075
2200
|
* means "old edge", not tampering — egress asserts ONLY when the claim is
|
|
2076
2201
|
* present. Make required once a fleet is reliably past deploy.
|
|
2077
2202
|
*/
|
|
2078
|
-
method:
|
|
2079
|
-
path:
|
|
2203
|
+
method: z26.string().min(1).optional(),
|
|
2204
|
+
path: z26.string().optional(),
|
|
2080
2205
|
/**
|
|
2081
2206
|
* Environment tier this call is scoped to (dev-mode design §6). Egress resolves
|
|
2082
2207
|
* the connection secret within this tier — a `dev` instruction can never reach a
|
|
@@ -2105,7 +2230,7 @@ var INSTRUCTION_TTL_SECONDS = 30;
|
|
|
2105
2230
|
var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
|
|
2106
2231
|
|
|
2107
2232
|
// ../shared/src/fetch.ts
|
|
2108
|
-
import { z as
|
|
2233
|
+
import { z as z27 } from "zod";
|
|
2109
2234
|
var FETCH_ERROR_CODES = [
|
|
2110
2235
|
"forbidden",
|
|
2111
2236
|
"rate_limited",
|
|
@@ -2118,26 +2243,26 @@ var FETCH_ERROR_CODES = [
|
|
|
2118
2243
|
"provider_unavailable",
|
|
2119
2244
|
"provider_misconfigured"
|
|
2120
2245
|
];
|
|
2121
|
-
var FetchErrorCodeSchema =
|
|
2122
|
-
var FetchErrorProviderSchema =
|
|
2246
|
+
var FetchErrorCodeSchema = z27.enum(FETCH_ERROR_CODES);
|
|
2247
|
+
var FetchErrorProviderSchema = z27.strictObject({
|
|
2123
2248
|
ref: ProviderRefSchema,
|
|
2124
|
-
displayName:
|
|
2249
|
+
displayName: z27.string().min(1).max(200).optional()
|
|
2125
2250
|
});
|
|
2126
|
-
var FetchProxyErrorSchema =
|
|
2251
|
+
var FetchProxyErrorSchema = z27.object({
|
|
2127
2252
|
code: FetchErrorCodeSchema,
|
|
2128
|
-
message:
|
|
2253
|
+
message: z27.string(),
|
|
2129
2254
|
/** Present only on the delegated-call codes that carry it (`connection_required`, `provider_unavailable`). */
|
|
2130
2255
|
provider: FetchErrorProviderSchema.optional()
|
|
2131
2256
|
});
|
|
2132
2257
|
|
|
2133
2258
|
// ../shared/src/sessions.ts
|
|
2134
|
-
import { z as
|
|
2135
|
-
var SessionPrincipalKindSchema =
|
|
2136
|
-
var SessionSummarySchema =
|
|
2137
|
-
id:
|
|
2138
|
-
appId:
|
|
2259
|
+
import { z as z28 } from "zod";
|
|
2260
|
+
var SessionPrincipalKindSchema = z28.enum(["user", "password"]);
|
|
2261
|
+
var SessionSummarySchema = z28.object({
|
|
2262
|
+
id: z28.uuid(),
|
|
2263
|
+
appId: z28.uuid(),
|
|
2139
2264
|
/** App slug at read time; the FK guarantees the app row exists. */
|
|
2140
|
-
slug:
|
|
2265
|
+
slug: z28.string(),
|
|
2141
2266
|
/**
|
|
2142
2267
|
* The identity half — Entra's `oid` claim since ADR-0048 (a pairwise `sub`
|
|
2143
2268
|
* on rows that predate the re-base; a `pw_*` pseudonym for shared-password
|
|
@@ -2145,55 +2270,63 @@ var SessionSummarySchema = z26.object({
|
|
|
2145
2270
|
* resort: the platform deliberately holds no Graph `/users` grant, so no
|
|
2146
2271
|
* principal id it stores resolves to a name.
|
|
2147
2272
|
*/
|
|
2148
|
-
userOid:
|
|
2273
|
+
userOid: z28.string(),
|
|
2149
2274
|
/**
|
|
2150
2275
|
* The display half, captured at login. Render, never compare. Null for
|
|
2151
2276
|
* shared-password visitors (each login is a fresh pseudonym) and for rows
|
|
2152
2277
|
* predating the columns.
|
|
2153
2278
|
*/
|
|
2154
|
-
userName:
|
|
2155
|
-
userEmail:
|
|
2279
|
+
userName: z28.string().nullable(),
|
|
2280
|
+
userEmail: z28.string().nullable(),
|
|
2156
2281
|
userKind: SessionPrincipalKindSchema.nullable(),
|
|
2157
2282
|
/** Group-id snapshot as of this session's login/refresh. */
|
|
2158
|
-
groups:
|
|
2159
|
-
createdAt:
|
|
2283
|
+
groups: z28.array(z28.string()),
|
|
2284
|
+
createdAt: z28.iso.datetime(),
|
|
2160
2285
|
/** Null while the handoff is un-redeemed — impossible here (live rows only). */
|
|
2161
|
-
activatedAt:
|
|
2286
|
+
activatedAt: z28.iso.datetime().nullable(),
|
|
2162
2287
|
/** When silent re-auth (`prompt=none`) becomes due; past = overdue. */
|
|
2163
|
-
refreshDueAt:
|
|
2288
|
+
refreshDueAt: z28.iso.datetime(),
|
|
2164
2289
|
/** Hard cap — the session is dead at this instant regardless of activity. */
|
|
2165
|
-
expiresAt:
|
|
2290
|
+
expiresAt: z28.iso.datetime()
|
|
2166
2291
|
});
|
|
2167
|
-
var SessionListResponseSchema =
|
|
2168
|
-
rows:
|
|
2169
|
-
groupNames:
|
|
2170
|
-
groupsResolved:
|
|
2292
|
+
var SessionListResponseSchema = z28.object({
|
|
2293
|
+
rows: z28.array(SessionSummarySchema),
|
|
2294
|
+
groupNames: z28.record(z28.string(), z28.string().nullable()),
|
|
2295
|
+
groupsResolved: z28.boolean()
|
|
2171
2296
|
});
|
|
2172
|
-
var SessionRevokeRequestSchema =
|
|
2297
|
+
var SessionRevokeRequestSchema = z28.object({
|
|
2173
2298
|
/**
|
|
2174
2299
|
* A session's principal id as seen in the list — the `oid` claim, a `pw_*`
|
|
2175
2300
|
* pseudonym, or a pre-re-base pairwise `sub`. Opaque to the admin; copied
|
|
2176
2301
|
* from the row they clicked, so its shape is whatever the session rows
|
|
2177
2302
|
* hold — bounded only against abuse, not parsed.
|
|
2178
2303
|
*/
|
|
2179
|
-
userOid:
|
|
2304
|
+
userOid: z28.string().min(1).max(200)
|
|
2180
2305
|
});
|
|
2181
|
-
var SessionRevokeResultSchema =
|
|
2182
|
-
removed:
|
|
2306
|
+
var SessionRevokeResultSchema = z28.object({
|
|
2307
|
+
removed: z28.int().nonnegative(),
|
|
2183
2308
|
/** Slugs of the apps the removed sessions lived on; empty when none. */
|
|
2184
|
-
apps:
|
|
2309
|
+
apps: z28.array(z28.string())
|
|
2185
2310
|
});
|
|
2186
2311
|
|
|
2187
2312
|
// src/client.ts
|
|
2188
2313
|
var CliError = class extends Error {
|
|
2189
2314
|
code;
|
|
2190
|
-
|
|
2315
|
+
/** The portal envelope's `details`, when it sent one. */
|
|
2316
|
+
details;
|
|
2317
|
+
constructor(message, code, details) {
|
|
2191
2318
|
super(message);
|
|
2192
2319
|
this.name = "CliError";
|
|
2193
2320
|
this.code = code;
|
|
2321
|
+
this.details = details;
|
|
2194
2322
|
}
|
|
2195
2323
|
};
|
|
2196
|
-
|
|
2324
|
+
function errorReason(err) {
|
|
2325
|
+
if (!(err instanceof CliError)) return void 0;
|
|
2326
|
+
const reason = err.details?.reason;
|
|
2327
|
+
return typeof reason === "string" ? reason : void 0;
|
|
2328
|
+
}
|
|
2329
|
+
var VersionListSchema = z29.array(VersionSchema);
|
|
2197
2330
|
var PortalClient = class {
|
|
2198
2331
|
#baseUrl;
|
|
2199
2332
|
#tokenProvider;
|
|
@@ -2219,6 +2352,34 @@ var PortalClient = class {
|
|
|
2219
2352
|
getAuthConfig() {
|
|
2220
2353
|
return this.#json(AuthConfigResponseSchema, "GET", "/api/v1/auth/config", {});
|
|
2221
2354
|
}
|
|
2355
|
+
/** Public deployment topology (`GET /api/v1/config`), incl. the CI token audience. */
|
|
2356
|
+
getDeploymentConfig() {
|
|
2357
|
+
return this.#json(DeploymentConfigResponseSchema, "GET", "/api/v1/config", {});
|
|
2358
|
+
}
|
|
2359
|
+
/** The app's GitHub binding (ADR-0051), or null when it has none. */
|
|
2360
|
+
async getRepoBinding(slug) {
|
|
2361
|
+
try {
|
|
2362
|
+
return await this.#json(RepoBindingSchema, "GET", `/api/v1/apps/${enc(slug)}/repo-binding`, {
|
|
2363
|
+
auth: true
|
|
2364
|
+
});
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
if (errorReason(err) === "no_binding") return null;
|
|
2367
|
+
throw err;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
putRepoBinding(slug, body) {
|
|
2371
|
+
return this.#json(RepoBindingSchema, "PUT", `/api/v1/apps/${enc(slug)}/repo-binding`, {
|
|
2372
|
+
auth: true,
|
|
2373
|
+
body
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
async deleteRepoBinding(slug) {
|
|
2377
|
+
const res = await fetch(this.#url(`/api/v1/apps/${enc(slug)}/repo-binding`), {
|
|
2378
|
+
method: "DELETE",
|
|
2379
|
+
headers: await this.#authHeaders(true)
|
|
2380
|
+
});
|
|
2381
|
+
if (!res.ok) await this.#parse(z29.unknown(), res);
|
|
2382
|
+
}
|
|
2222
2383
|
/** The authenticated actor, per the portal — powers `helix whoami`. */
|
|
2223
2384
|
me() {
|
|
2224
2385
|
return this.#json(PortalMeResponseSchema, "GET", "/api/v1/me", { auth: true });
|
|
@@ -2258,8 +2419,22 @@ var PortalClient = class {
|
|
|
2258
2419
|
});
|
|
2259
2420
|
return this.#parse(UploadVersionResponseSchema, res);
|
|
2260
2421
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2422
|
+
/**
|
|
2423
|
+
* Upload from GitHub Actions to the CI route (ADR-0051), authenticated by the
|
|
2424
|
+
* job's GitHub OIDC token rather than a Helix sign-in.
|
|
2425
|
+
*/
|
|
2426
|
+
async uploadCiVersion(slug, zip, githubToken) {
|
|
2427
|
+
const form = new FormData();
|
|
2428
|
+
form.append("bundle", new Blob([new Uint8Array(zip)]), "bundle.zip");
|
|
2429
|
+
const res = await fetch(this.#url(`/api/v1/apps/${enc(slug)}/ci/versions`), {
|
|
2430
|
+
method: "POST",
|
|
2431
|
+
headers: { authorization: `Bearer ${githubToken}` },
|
|
2432
|
+
body: form
|
|
2433
|
+
});
|
|
2434
|
+
return this.#parse(UploadVersionResponseSchema, res);
|
|
2435
|
+
}
|
|
2436
|
+
#url(path5) {
|
|
2437
|
+
return `${this.#baseUrl}${path5}`;
|
|
2263
2438
|
}
|
|
2264
2439
|
async #authHeaders(auth) {
|
|
2265
2440
|
if (!auth) return {};
|
|
@@ -2269,10 +2444,10 @@ var PortalClient = class {
|
|
|
2269
2444
|
}
|
|
2270
2445
|
return { authorization: `Bearer ${token}` };
|
|
2271
2446
|
}
|
|
2272
|
-
async #json(schema, method,
|
|
2447
|
+
async #json(schema, method, path5, opts) {
|
|
2273
2448
|
const headers = await this.#authHeaders(opts.auth ?? false);
|
|
2274
2449
|
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
2275
|
-
const res = await fetch(this.#url(
|
|
2450
|
+
const res = await fetch(this.#url(path5), {
|
|
2276
2451
|
method,
|
|
2277
2452
|
headers,
|
|
2278
2453
|
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
@@ -2284,20 +2459,26 @@ var PortalClient = class {
|
|
|
2284
2459
|
const data = text ? JSON.parse(text) : void 0;
|
|
2285
2460
|
if (!res.ok) {
|
|
2286
2461
|
const parsed = ApiErrorSchema.safeParse(data);
|
|
2287
|
-
if (parsed.success)
|
|
2462
|
+
if (parsed.success) {
|
|
2463
|
+
const { message, code, details } = parsed.data.error;
|
|
2464
|
+
throw new CliError(message, code, details);
|
|
2465
|
+
}
|
|
2288
2466
|
throw new CliError(`request failed (HTTP ${res.status})`);
|
|
2289
2467
|
}
|
|
2290
2468
|
return schema.parse(data);
|
|
2291
2469
|
}
|
|
2292
2470
|
/** A non-JSON GET (the markdown skill) — errors still typed via the envelope. */
|
|
2293
|
-
async #text(
|
|
2471
|
+
async #text(path5, opts) {
|
|
2294
2472
|
const headers = await this.#authHeaders(opts.auth ?? false);
|
|
2295
|
-
const res = await fetch(this.#url(
|
|
2473
|
+
const res = await fetch(this.#url(path5), { method: "GET", headers });
|
|
2296
2474
|
const body = await res.text();
|
|
2297
2475
|
if (!res.ok) {
|
|
2298
2476
|
const data = body ? safeJsonParse(body) : void 0;
|
|
2299
2477
|
const parsed = ApiErrorSchema.safeParse(data);
|
|
2300
|
-
if (parsed.success)
|
|
2478
|
+
if (parsed.success) {
|
|
2479
|
+
const { message, code, details } = parsed.data.error;
|
|
2480
|
+
throw new CliError(message, code, details);
|
|
2481
|
+
}
|
|
2301
2482
|
throw new CliError(`request failed (HTTP ${res.status})`);
|
|
2302
2483
|
}
|
|
2303
2484
|
return body;
|
|
@@ -2444,6 +2625,28 @@ async function deleteTokens(portalUrl, file = defaultTokenPath()) {
|
|
|
2444
2625
|
return true;
|
|
2445
2626
|
}
|
|
2446
2627
|
|
|
2628
|
+
// src/auth/githubActions.ts
|
|
2629
|
+
import { z as z30 } from "zod";
|
|
2630
|
+
function githubTokenSource(env) {
|
|
2631
|
+
const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
2632
|
+
const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
2633
|
+
return requestUrl && requestToken ? { requestUrl, requestToken } : null;
|
|
2634
|
+
}
|
|
2635
|
+
var IdTokenResponseSchema = z30.object({ value: z30.string().min(1) });
|
|
2636
|
+
async function requestGithubIdToken(source, audience, fetchImpl = fetch) {
|
|
2637
|
+
const url = new URL(source.requestUrl);
|
|
2638
|
+
url.searchParams.set("audience", audience);
|
|
2639
|
+
const res = await fetchImpl(url, {
|
|
2640
|
+
headers: { authorization: `Bearer ${source.requestToken}`, accept: "application/json" }
|
|
2641
|
+
});
|
|
2642
|
+
if (!res.ok) {
|
|
2643
|
+
throw new CliError(`GitHub refused the ID token request (HTTP ${res.status})`);
|
|
2644
|
+
}
|
|
2645
|
+
const parsed = IdTokenResponseSchema.safeParse(await res.json());
|
|
2646
|
+
if (!parsed.success) throw new CliError("GitHub's ID token response had no token");
|
|
2647
|
+
return parsed.data.value;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2447
2650
|
// src/commands.ts
|
|
2448
2651
|
function requireSlug(config) {
|
|
2449
2652
|
if (!config.slug) {
|
|
@@ -2498,13 +2701,53 @@ async function createCommand(client, config, opts) {
|
|
|
2498
2701
|
console.log(`Created app "${app.slug}".`);
|
|
2499
2702
|
printApp(app);
|
|
2500
2703
|
}
|
|
2501
|
-
async function deployCommand(client, config, opts) {
|
|
2704
|
+
async function deployCommand(client, config, opts, env = process.env) {
|
|
2502
2705
|
const slug = requireSlug(config);
|
|
2706
|
+
const github = config.token ? null : githubTokenSource(env);
|
|
2707
|
+
if (!config.token && !github && env.GITHUB_ACTIONS === "true") {
|
|
2708
|
+
throw new CliError(
|
|
2709
|
+
"running in GitHub Actions without an ID token; add `permissions: id-token: write` to the workflow (see `helix ci init`)"
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
if (github && !isSafePortalUrl(config.portalUrl)) {
|
|
2713
|
+
throw new CliError(
|
|
2714
|
+
`won't send a GitHub token over plain http (${config.portalUrl}); use the portal's https URL`
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
if (github && opts.promote) {
|
|
2718
|
+
throw new CliError(
|
|
2719
|
+
"CI deploys land as a preview; promote it in the portal or with `helix promote <n>`"
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2503
2722
|
const zip = config.bundle ? await readFile2(config.bundle) : await zipDirectory(config.dir);
|
|
2504
2723
|
console.log(`Uploading bundle to "${slug}"\u2026`);
|
|
2505
|
-
|
|
2724
|
+
let result;
|
|
2725
|
+
if (github) {
|
|
2726
|
+
const deployment = await client.getDeploymentConfig();
|
|
2727
|
+
if (!deployment.repoDeploy?.enabled) {
|
|
2728
|
+
throw new CliError("this portal does not accept repo-backed deploys (ask its operator)");
|
|
2729
|
+
}
|
|
2730
|
+
const idToken = await requestGithubIdToken(github, deployment.repoDeploy.audience);
|
|
2731
|
+
try {
|
|
2732
|
+
result = await client.uploadCiVersion(slug, zip, idToken);
|
|
2733
|
+
} catch (err) {
|
|
2734
|
+
if (err instanceof CliError && err.code === "forbidden" && !errorReason(err)) {
|
|
2735
|
+
throw new CliError(
|
|
2736
|
+
`${err.message}; check that "${slug}" is connected to this repo (helix ci show)`,
|
|
2737
|
+
err.code
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
throw err;
|
|
2741
|
+
}
|
|
2742
|
+
} else {
|
|
2743
|
+
result = await client.uploadVersion(slug, zip);
|
|
2744
|
+
}
|
|
2745
|
+
const { version, warnings } = result;
|
|
2506
2746
|
console.log(`Uploaded as preview:`);
|
|
2507
2747
|
printVersion(version);
|
|
2748
|
+
if (version.source) {
|
|
2749
|
+
console.log(` from: ${version.source.repoFullName}@${version.source.commitSha.slice(0, 7)}`);
|
|
2750
|
+
}
|
|
2508
2751
|
if (warnings.length > 0) {
|
|
2509
2752
|
console.log(`
|
|
2510
2753
|
${warnings.length} CSP warning(s):`);
|
|
@@ -2520,6 +2763,10 @@ Promoted version ${version.number} to live.`);
|
|
|
2520
2763
|
Not live yet \u2014 promote with: helix promote ${version.number}`);
|
|
2521
2764
|
}
|
|
2522
2765
|
}
|
|
2766
|
+
function isSafePortalUrl(url) {
|
|
2767
|
+
const { protocol, hostname } = new URL(url);
|
|
2768
|
+
return protocol === "https:" || protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(hostname);
|
|
2769
|
+
}
|
|
2523
2770
|
async function versionsCommand(client, config) {
|
|
2524
2771
|
const slug = requireSlug(config);
|
|
2525
2772
|
const versions = await client.listVersions(slug);
|
|
@@ -2615,6 +2862,12 @@ function makeTokenProvider(opts, deps = defaultDeps) {
|
|
|
2615
2862
|
};
|
|
2616
2863
|
}
|
|
2617
2864
|
|
|
2865
|
+
// src/ci/commands.ts
|
|
2866
|
+
import { execFile } from "node:child_process";
|
|
2867
|
+
import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
2868
|
+
import path4 from "node:path";
|
|
2869
|
+
import { promisify } from "node:util";
|
|
2870
|
+
|
|
2618
2871
|
// src/config.ts
|
|
2619
2872
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
2620
2873
|
import path2 from "node:path";
|
|
@@ -2644,6 +2897,341 @@ async function resolveConfig(flags, env = process.env, cwd = process.cwd()) {
|
|
|
2644
2897
|
};
|
|
2645
2898
|
}
|
|
2646
2899
|
|
|
2900
|
+
// src/version.ts
|
|
2901
|
+
var CLI_VERSION = true ? "0.5.0" : "latest";
|
|
2902
|
+
|
|
2903
|
+
// src/ci/remote.ts
|
|
2904
|
+
function parseGithubRemote(remote, githubHost = "github.com") {
|
|
2905
|
+
const url = remote.trim();
|
|
2906
|
+
const scp = /^[\w.-]+@([^:/]+):(.+)$/.exec(url);
|
|
2907
|
+
let host;
|
|
2908
|
+
let path5;
|
|
2909
|
+
if (scp && !url.includes("://")) {
|
|
2910
|
+
host = scp[1];
|
|
2911
|
+
path5 = scp[2];
|
|
2912
|
+
} else {
|
|
2913
|
+
let parsed;
|
|
2914
|
+
try {
|
|
2915
|
+
parsed = new URL(url);
|
|
2916
|
+
} catch {
|
|
2917
|
+
return null;
|
|
2918
|
+
}
|
|
2919
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(parsed.protocol)) return null;
|
|
2920
|
+
host = parsed.hostname;
|
|
2921
|
+
path5 = parsed.pathname.replace(/^\/+/, "");
|
|
2922
|
+
}
|
|
2923
|
+
if (host.toLowerCase() !== githubHost.toLowerCase()) return null;
|
|
2924
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9-]{0,38})\/([A-Za-z0-9._-]+?)(\.git)?\/?$/.exec(path5);
|
|
2925
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// src/ci/workflow.ts
|
|
2929
|
+
import { access, readFile as readFile4 } from "node:fs/promises";
|
|
2930
|
+
import path3 from "node:path";
|
|
2931
|
+
var REUSABLE_WORKFLOW_PATH = "AZX-PBC-OSS/helix-actions/.github/workflows/deploy.yml";
|
|
2932
|
+
var REUSABLE_WORKFLOW_TAG = "v1";
|
|
2933
|
+
var REUSABLE_WORKFLOW_REF = `${REUSABLE_WORKFLOW_PATH}@refs/tags/${REUSABLE_WORKFLOW_TAG}`;
|
|
2934
|
+
var WORKFLOW_FILE = ".github/workflows/helix-deploy.yml";
|
|
2935
|
+
var DEFAULT_PNPM_VERSION = "10";
|
|
2936
|
+
async function exists(file) {
|
|
2937
|
+
try {
|
|
2938
|
+
await access(file);
|
|
2939
|
+
return true;
|
|
2940
|
+
} catch {
|
|
2941
|
+
return false;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
var LOCKFILES = [
|
|
2945
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
2946
|
+
["yarn.lock", "yarn"],
|
|
2947
|
+
["package-lock.json", "npm"]
|
|
2948
|
+
];
|
|
2949
|
+
var posix = (p) => p.split(path3.sep).join("/");
|
|
2950
|
+
async function readPackageJson(file) {
|
|
2951
|
+
try {
|
|
2952
|
+
return JSON.parse(await readFile4(file, "utf8"));
|
|
2953
|
+
} catch {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
async function detectBuildPlan(appDir, repoRoot) {
|
|
2958
|
+
let packageManager = "npm";
|
|
2959
|
+
let lockfilePath = null;
|
|
2960
|
+
search: for (const dir of [appDir, repoRoot]) {
|
|
2961
|
+
for (const [name, pm] of LOCKFILES) {
|
|
2962
|
+
if (await exists(path3.join(dir, name))) {
|
|
2963
|
+
packageManager = pm;
|
|
2964
|
+
lockfilePath = posix(path3.relative(repoRoot, path3.join(dir, name)));
|
|
2965
|
+
break search;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
const appPkg = await readPackageJson(path3.join(appDir, "package.json"));
|
|
2970
|
+
let pnpmPackageJson = null;
|
|
2971
|
+
if (packageManager === "pnpm") {
|
|
2972
|
+
for (const dir of [appDir, repoRoot]) {
|
|
2973
|
+
const pkg = dir === appDir ? appPkg : await readPackageJson(path3.join(dir, "package.json"));
|
|
2974
|
+
if (typeof pkg?.packageManager === "string" && pkg.packageManager.startsWith("pnpm@")) {
|
|
2975
|
+
pnpmPackageJson = posix(path3.relative(repoRoot, path3.join(dir, "package.json")));
|
|
2976
|
+
break;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const buildScript = typeof appPkg?.scripts?.build === "string" ? "build" : null;
|
|
2981
|
+
return {
|
|
2982
|
+
packageManager,
|
|
2983
|
+
hasLockfile: lockfilePath !== null,
|
|
2984
|
+
lockfilePath,
|
|
2985
|
+
pnpmPackageJson,
|
|
2986
|
+
buildScript
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
var INSTALL = {
|
|
2990
|
+
npm: "npm ci",
|
|
2991
|
+
pnpm: "pnpm install --frozen-lockfile",
|
|
2992
|
+
yarn: "yarn install --frozen-lockfile"
|
|
2993
|
+
};
|
|
2994
|
+
var HEADER = (branch) => `# Deploys this app to Helix as a preview on every push to ${branch}.
|
|
2995
|
+
# Written by \`helix ci init\`. The repo needs no secret: the deploy job
|
|
2996
|
+
# authenticates with its own GitHub OIDC token. The build job runs the repo's
|
|
2997
|
+
# code without that permission. A preview goes live only when someone promotes
|
|
2998
|
+
# it, in the portal or with \`helix promote <n>\`.
|
|
2999
|
+
name: Deploy to Helix
|
|
3000
|
+
on:
|
|
3001
|
+
push:
|
|
3002
|
+
branches: [${JSON.stringify(branch)}]
|
|
3003
|
+
workflow_dispatch:
|
|
3004
|
+
# Each job asks for only what it needs.
|
|
3005
|
+
permissions: {}
|
|
3006
|
+
`;
|
|
3007
|
+
var inDir = (workingDirectory, rel) => workingDirectory === "." ? rel : `${workingDirectory}/${rel}`;
|
|
3008
|
+
var FIND_OUTPUT = `if [ -f helix.json ]; then cfg=helix.json; elif [ -f azx.json ]; then cfg=azx.json; else echo "::error::no helix.json in this directory"; exit 1; fi
|
|
3009
|
+
echo "config=$cfg" >> "$GITHUB_OUTPUT"
|
|
3010
|
+
echo "dir=$(node -p "require('./' + process.argv[1]).dir || 'dist'" "$cfg")" >> "$GITHUB_OUTPUT"`;
|
|
3011
|
+
function renderStarterWorkflow(opts) {
|
|
3012
|
+
const { plan, workingDirectory: wd } = opts;
|
|
3013
|
+
const sub = wd !== ".";
|
|
3014
|
+
const install = plan.hasLockfile ? INSTALL[plan.packageManager] : "npm install";
|
|
3015
|
+
const pnpmSetup = plan.packageManager !== "pnpm" ? [] : [
|
|
3016
|
+
" - uses: pnpm/action-setup@v4",
|
|
3017
|
+
" with:",
|
|
3018
|
+
plan.pnpmPackageJson ? ` package_json_file: ${plan.pnpmPackageJson}` : ` version: ${DEFAULT_PNPM_VERSION}`
|
|
3019
|
+
];
|
|
3020
|
+
const cache = plan.lockfilePath ? [
|
|
3021
|
+
` cache: ${plan.packageManager}`,
|
|
3022
|
+
` cache-dependency-path: ${plan.lockfilePath}`
|
|
3023
|
+
] : [];
|
|
3024
|
+
const lines = [
|
|
3025
|
+
HEADER(opts.branch),
|
|
3026
|
+
"jobs:",
|
|
3027
|
+
" build:",
|
|
3028
|
+
" runs-on: ubuntu-latest",
|
|
3029
|
+
" permissions:",
|
|
3030
|
+
" contents: read",
|
|
3031
|
+
...sub ? [" defaults:", " run:", ` working-directory: ${JSON.stringify(wd)}`] : [],
|
|
3032
|
+
" steps:",
|
|
3033
|
+
" - uses: actions/checkout@v4",
|
|
3034
|
+
...pnpmSetup,
|
|
3035
|
+
" - uses: actions/setup-node@v4",
|
|
3036
|
+
" with:",
|
|
3037
|
+
" node-version: 24",
|
|
3038
|
+
...cache,
|
|
3039
|
+
` - run: ${install}`,
|
|
3040
|
+
...plan.buildScript ? [` - run: ${plan.packageManager} run ${plan.buildScript}`] : [],
|
|
3041
|
+
" - id: output",
|
|
3042
|
+
" run: |",
|
|
3043
|
+
` ${FIND_OUTPUT}`,
|
|
3044
|
+
" - uses: actions/upload-artifact@v4",
|
|
3045
|
+
" with:",
|
|
3046
|
+
" name: helix-bundle",
|
|
3047
|
+
" path: |",
|
|
3048
|
+
` ${inDir(wd, "${{ steps.output.outputs.config }}")}`,
|
|
3049
|
+
` ${inDir(wd, "${{ steps.output.outputs.dir }}")}`,
|
|
3050
|
+
" include-hidden-files: true",
|
|
3051
|
+
" if-no-files-found: error",
|
|
3052
|
+
" deploy:",
|
|
3053
|
+
" needs: build",
|
|
3054
|
+
" runs-on: ubuntu-latest",
|
|
3055
|
+
" permissions:",
|
|
3056
|
+
" id-token: write",
|
|
3057
|
+
" steps:",
|
|
3058
|
+
" - uses: actions/download-artifact@v4",
|
|
3059
|
+
" with:",
|
|
3060
|
+
" name: helix-bundle",
|
|
3061
|
+
` path: ${JSON.stringify(wd)}`,
|
|
3062
|
+
" - uses: actions/setup-node@v4",
|
|
3063
|
+
" with:",
|
|
3064
|
+
" node-version: 24",
|
|
3065
|
+
` - run: npx --yes @azx-pbc/helix-cli@${opts.cliVersion} deploy`,
|
|
3066
|
+
...sub ? [` working-directory: ${JSON.stringify(wd)}`] : [],
|
|
3067
|
+
""
|
|
3068
|
+
];
|
|
3069
|
+
return lines.join("\n");
|
|
3070
|
+
}
|
|
3071
|
+
function renderReusableWorkflow(opts) {
|
|
3072
|
+
const { plan } = opts;
|
|
3073
|
+
const lines = [
|
|
3074
|
+
HEADER(opts.branch),
|
|
3075
|
+
"jobs:",
|
|
3076
|
+
" deploy:",
|
|
3077
|
+
` uses: ${REUSABLE_WORKFLOW_PATH}@${REUSABLE_WORKFLOW_TAG}`,
|
|
3078
|
+
// A caller grants the reusable workflow at most these; its own jobs narrow
|
|
3079
|
+
// them per job.
|
|
3080
|
+
" permissions:",
|
|
3081
|
+
" contents: read",
|
|
3082
|
+
" id-token: write",
|
|
3083
|
+
" with:",
|
|
3084
|
+
` package-manager: ${plan.packageManager}`,
|
|
3085
|
+
` working-directory: ${JSON.stringify(opts.workingDirectory)}`,
|
|
3086
|
+
` build-script: ${JSON.stringify(plan.buildScript ?? "")}`,
|
|
3087
|
+
...plan.packageManager === "pnpm" && !plan.pnpmPackageJson ? [` pnpm-version: ${JSON.stringify(DEFAULT_PNPM_VERSION)}`] : [],
|
|
3088
|
+
` cli-version: ${JSON.stringify(opts.cliVersion)}`,
|
|
3089
|
+
""
|
|
3090
|
+
];
|
|
3091
|
+
return lines.join("\n");
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
// src/ci/commands.ts
|
|
3095
|
+
var run = promisify(execFile);
|
|
3096
|
+
var defaultGit = async (args, cwd) => (await run("git", args, { cwd })).stdout.trim();
|
|
3097
|
+
function requireSlug2(config) {
|
|
3098
|
+
if (!config.slug) throw new CliError("no app slug; set it in helix.json or pass --slug");
|
|
3099
|
+
return config.slug;
|
|
3100
|
+
}
|
|
3101
|
+
async function ciInitCommand(client, config, opts, deps = {}) {
|
|
3102
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
3103
|
+
const git = deps.git ?? defaultGit;
|
|
3104
|
+
const log = deps.log ?? console.log;
|
|
3105
|
+
const slug = requireSlug2(config);
|
|
3106
|
+
const deployment = await client.getDeploymentConfig();
|
|
3107
|
+
if (!deployment.repoDeploy?.enabled) {
|
|
3108
|
+
throw new CliError("this portal does not accept repo-backed deploys (ask its operator)");
|
|
3109
|
+
}
|
|
3110
|
+
const githubHost = new URL(deployment.repoDeploy.serverUrl).hostname;
|
|
3111
|
+
let repoRoot;
|
|
3112
|
+
try {
|
|
3113
|
+
repoRoot = await git(["rev-parse", "--show-toplevel"], cwd);
|
|
3114
|
+
} catch {
|
|
3115
|
+
throw new CliError("not inside a git checkout; run this from the app's directory in its repo");
|
|
3116
|
+
}
|
|
3117
|
+
const repo = opts.repo ?? parseGithubRemote(await originUrl(git, cwd), githubHost);
|
|
3118
|
+
if (!repo) {
|
|
3119
|
+
throw new CliError(
|
|
3120
|
+
`couldn't read a ${githubHost} repo from the origin remote; pass --repo owner/name`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
const branch = opts.branch ?? await defaultBranch(git, cwd);
|
|
3124
|
+
if (!branch || branch === "HEAD") {
|
|
3125
|
+
throw new CliError("couldn't tell which branch to deploy from; pass --branch <name>");
|
|
3126
|
+
}
|
|
3127
|
+
const workflowPath = path4.join(repoRoot, WORKFLOW_FILE);
|
|
3128
|
+
if (!opts.force && await fileExists(workflowPath)) {
|
|
3129
|
+
throw new CliError(`${WORKFLOW_FILE} already exists; pass --force to replace it`);
|
|
3130
|
+
}
|
|
3131
|
+
const workingDirectory = path4.relative(repoRoot, cwd).split(path4.sep).join("/") || ".";
|
|
3132
|
+
const plan = await detectBuildPlan(cwd, repoRoot);
|
|
3133
|
+
const render = opts.reusable ? renderReusableWorkflow : renderStarterWorkflow;
|
|
3134
|
+
const workflow = render({ branch, workingDirectory, plan, cliVersion: CLI_VERSION });
|
|
3135
|
+
const existing = await client.getRepoBinding(slug);
|
|
3136
|
+
const workflowRef = opts.reusable ? REUSABLE_WORKFLOW_REF : existing?.workflowRef;
|
|
3137
|
+
const environment = existing?.environment;
|
|
3138
|
+
if (existing?.workflowRef && workflowRef !== existing.workflowRef && !opts.force) {
|
|
3139
|
+
throw new CliError(
|
|
3140
|
+
`the binding pins deploys to workflow ${existing.workflowRef}; --reusable would replace that pin. Pass --force to replace it.`
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
const binding = await client.putRepoBinding(slug, {
|
|
3144
|
+
repoFullName: repo,
|
|
3145
|
+
branch,
|
|
3146
|
+
...workflowRef ? { workflowRef } : {},
|
|
3147
|
+
...environment ? { environment } : {}
|
|
3148
|
+
});
|
|
3149
|
+
const configFile = await writeHelixJson(cwd, {
|
|
3150
|
+
slug,
|
|
3151
|
+
portalUrl: config.portalUrl,
|
|
3152
|
+
dir: config.dir
|
|
3153
|
+
});
|
|
3154
|
+
await mkdir2(path4.dirname(workflowPath), { recursive: true });
|
|
3155
|
+
await writeFile3(workflowPath, workflow, "utf8");
|
|
3156
|
+
log(`Connected "${slug}" to ${repo}.`);
|
|
3157
|
+
log(`Branch: ${branch}${opts.branch ? "" : " (use --branch to change)"}`);
|
|
3158
|
+
printBinding(binding, log);
|
|
3159
|
+
log(`Wrote ${path4.relative(cwd, workflowPath) || WORKFLOW_FILE} and ${configFile}.`);
|
|
3160
|
+
if (isLocalPortal(config.portalUrl)) {
|
|
3161
|
+
log(`warning: helix.json points CI at ${config.portalUrl}, which a GitHub runner can't reach.`);
|
|
3162
|
+
}
|
|
3163
|
+
log("Commit both files and push; each push lands a preview to promote.");
|
|
3164
|
+
}
|
|
3165
|
+
async function ciShowCommand(client, config, log = console.log) {
|
|
3166
|
+
const slug = requireSlug2(config);
|
|
3167
|
+
const binding = await client.getRepoBinding(slug);
|
|
3168
|
+
if (!binding) {
|
|
3169
|
+
log(`"${slug}" is not connected to a repo. Run: helix ci init`);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
printBinding(binding, log);
|
|
3173
|
+
}
|
|
3174
|
+
async function ciRemoveCommand(client, config, log = console.log) {
|
|
3175
|
+
const slug = requireSlug2(config);
|
|
3176
|
+
await client.deleteRepoBinding(slug);
|
|
3177
|
+
log(`Disconnected "${slug}" from its repo. CI deploys to it are now refused.`);
|
|
3178
|
+
}
|
|
3179
|
+
function printBinding(b, log) {
|
|
3180
|
+
log(` repo: ${b.repoFullName}`);
|
|
3181
|
+
log(` branch: ${b.branch}`);
|
|
3182
|
+
if (b.workflowRef) log(` workflow: ${b.workflowRef}`);
|
|
3183
|
+
if (b.environment) log(` environment: ${b.environment}`);
|
|
3184
|
+
log(
|
|
3185
|
+
b.repositoryId ? ` pinned: repo id ${b.repositoryId}, owner id ${b.repositoryOwnerId ?? "?"}` : " pinned: not yet (the first deploy pins the repo's ids)"
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
async function originUrl(git, cwd) {
|
|
3189
|
+
try {
|
|
3190
|
+
return await git(["remote", "get-url", "origin"], cwd);
|
|
3191
|
+
} catch {
|
|
3192
|
+
return "";
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
async function defaultBranch(git, cwd) {
|
|
3196
|
+
try {
|
|
3197
|
+
const remoteHead = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
3198
|
+
if (remoteHead.startsWith("origin/")) return remoteHead.slice("origin/".length);
|
|
3199
|
+
} catch {
|
|
3200
|
+
}
|
|
3201
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
3202
|
+
}
|
|
3203
|
+
async function fileExists(file) {
|
|
3204
|
+
try {
|
|
3205
|
+
await readFile5(file);
|
|
3206
|
+
return true;
|
|
3207
|
+
} catch {
|
|
3208
|
+
return false;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
async function writeHelixJson(cwd, values) {
|
|
3212
|
+
for (const name2 of CONFIG_FILENAMES) {
|
|
3213
|
+
const file = path4.join(cwd, name2);
|
|
3214
|
+
let existing;
|
|
3215
|
+
try {
|
|
3216
|
+
existing = JSON.parse(await readFile5(file, "utf8"));
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
if (err.code === "ENOENT") continue;
|
|
3219
|
+
throw err;
|
|
3220
|
+
}
|
|
3221
|
+
await writeFile3(file, `${JSON.stringify({ ...existing, ...values }, null, 2)}
|
|
3222
|
+
`, "utf8");
|
|
3223
|
+
return name2;
|
|
3224
|
+
}
|
|
3225
|
+
const name = CONFIG_FILENAMES[0];
|
|
3226
|
+
await writeFile3(path4.join(cwd, name), `${JSON.stringify(values, null, 2)}
|
|
3227
|
+
`, "utf8");
|
|
3228
|
+
return name;
|
|
3229
|
+
}
|
|
3230
|
+
function isLocalPortal(url) {
|
|
3231
|
+
const host = new URL(url).hostname;
|
|
3232
|
+
return host === "localhost" || host === "127.0.0.1" || host.endsWith(".local") || host === "::1";
|
|
3233
|
+
}
|
|
3234
|
+
|
|
2647
3235
|
// src/bin.ts
|
|
2648
3236
|
var USAGE = `helix \u2014 Helix deploy CLI
|
|
2649
3237
|
|
|
@@ -2657,9 +3245,13 @@ Usage:
|
|
|
2657
3245
|
helix promote <number>
|
|
2658
3246
|
helix rollback [number]
|
|
2659
3247
|
helix skill [--path <file>] # write this deployment's agent skill to disk
|
|
3248
|
+
helix ci init [--repo <owner/name>] [--branch <b>] [--reusable] [--force]
|
|
3249
|
+
# connect the app to its GitHub repo and write the workflow
|
|
3250
|
+
helix ci show | helix ci remove
|
|
2660
3251
|
|
|
2661
3252
|
Common flags: --slug <slug> --portal-url <url> --token <token>
|
|
2662
|
-
Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login;
|
|
3253
|
+
Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; local scripts).
|
|
3254
|
+
In GitHub Actions, deploy uses the job's OIDC token (permissions: id-token: write).
|
|
2663
3255
|
Config file: helix.json { slug, portalUrl, dir, description }
|
|
2664
3256
|
Visibility: internal | group:<id>[,<id>...] | password | public
|
|
2665
3257
|
`;
|
|
@@ -2704,6 +3296,24 @@ async function main() {
|
|
|
2704
3296
|
visibility: values.visibility
|
|
2705
3297
|
});
|
|
2706
3298
|
break;
|
|
3299
|
+
case "ci": {
|
|
3300
|
+
const sub = positionals[1];
|
|
3301
|
+
if (sub === "init") {
|
|
3302
|
+
await ciInitCommand(client, config, {
|
|
3303
|
+
repo: values.repo,
|
|
3304
|
+
branch: values.branch,
|
|
3305
|
+
reusable: values.reusable,
|
|
3306
|
+
force: values.force
|
|
3307
|
+
});
|
|
3308
|
+
} else if (sub === "show") {
|
|
3309
|
+
await ciShowCommand(client, config);
|
|
3310
|
+
} else if (sub === "remove") {
|
|
3311
|
+
await ciRemoveCommand(client, config);
|
|
3312
|
+
} else {
|
|
3313
|
+
throw new CliError("usage: helix ci init | show | remove");
|
|
3314
|
+
}
|
|
3315
|
+
break;
|
|
3316
|
+
}
|
|
2707
3317
|
case "versions":
|
|
2708
3318
|
await versionsCommand(client, config);
|
|
2709
3319
|
break;
|