@mevdragon/vidfarm-devcli 0.2.5 → 0.2.6
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 +1 -1
- package/SKILL.developer.md +43 -6
- package/dist/src/app.js +16 -3
- package/dist/src/cli.js +5 -3
- package/dist/src/db.js +48 -0
- package/dist/src/services/template-sources.js +123 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ The local CLI runs the same template contract used by the hosted platform, but w
|
|
|
51
51
|
|
|
52
52
|
Use `vidfarm session` to print reusable auth headers and a sample `curl` request for the local REST API.
|
|
53
53
|
|
|
54
|
-
For hosted preview-media uploads, use `vidfarm-devcli presign-preview-media`. It calls the authenticated Vidfarm API with `VIDFARM_API_KEY`, mints a presigned PUT URL under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns a public-read media URL for the uploaded object.
|
|
54
|
+
For hosted preview-media uploads, use `vidfarm-devcli presign-preview-media`. It calls the authenticated Vidfarm API with `VIDFARM_API_KEY`, mints a presigned PUT URL under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns a public-read media URL for the uploaded object. Public readability is granted by bucket policy for `developer/*`, not by public write or list access.
|
|
55
55
|
|
|
56
56
|
## What The Hosted Platform Expects From Templates
|
|
57
57
|
|
package/SKILL.developer.md
CHANGED
|
@@ -26,6 +26,24 @@ Do not use this skill for:
|
|
|
26
26
|
- production deploys, release admin, or shared cloud infrastructure
|
|
27
27
|
- asking for AWS, S3, Remotion cloud, or other platform secrets
|
|
28
28
|
|
|
29
|
+
## Third-Party Publish Rule
|
|
30
|
+
|
|
31
|
+
Third-party template developers publish template code by pushing git commits to GitHub. That is the only code-distribution step they own.
|
|
32
|
+
|
|
33
|
+
The third-party developer responsibility is:
|
|
34
|
+
|
|
35
|
+
- build and validate the template locally
|
|
36
|
+
- commit and push the template folder to GitHub
|
|
37
|
+
- register or update the hosted template source metadata so Vidfarm knows which repo, branch, and template folder to import from
|
|
38
|
+
|
|
39
|
+
The third-party developer should not:
|
|
40
|
+
|
|
41
|
+
- run `import-source-prod`
|
|
42
|
+
- use AWS CloudFormation, SSM, EC2, or shared prod credentials
|
|
43
|
+
- treat platform import/activation as part of template authoring
|
|
44
|
+
|
|
45
|
+
`import-source-prod` and `deploy-template-cycle` are platform-operator commands. They are for internal admin flow, not for external template authors.
|
|
46
|
+
|
|
29
47
|
For third-party developers, Remotion is local and storage should be accessed through Vidfarm helpers. Do not require cloud Remotion setup or hand-written S3 integration for normal template work.
|
|
30
48
|
|
|
31
49
|
## Agent Operating Rule
|
|
@@ -101,17 +119,36 @@ Notes:
|
|
|
101
119
|
- template authoring is TypeScript-first; create and edit template source as `src/template.ts`, not `src/template.js`
|
|
102
120
|
- keep exploratory scripts, one-off test runners, scratch validation files, and temporary job-driving code out of `src/`; place them in a repo-local tmp folder that is a sibling to `src` instead, for example `templates/vidfarm_template_0007/tmp/`
|
|
103
121
|
- when registering a hosted template source, set `template_module_path` to the repo-relative TypeScript entrypoint inside the template folder, for example `templates/vidfarm_template_example/src/template.ts`; the platform build/import flow resolves the compiled `src/template.js` sibling after `npm run build`
|
|
122
|
+
- hosted template registration is git metadata only: repo URL, branch, template entrypoint path, and optionally an explicit commit SHA for a specific import
|
|
123
|
+
- if a template moves folders or switches branches, update the registration metadata; if the template code changed but the location stayed the same, just push a new Git commit
|
|
124
|
+
- the platform determines whether an update exists by comparing the registered branch head against the latest imported release commit
|
|
104
125
|
- do not ask for platform secrets like `ENCRYPTION_SECRET`, `API_KEY_SALT`, `WEBHOOK_SECRET`, admin emails, S3 config, or generic AWS credentials
|
|
105
126
|
- do not ask for `REMOTION_AWS_ACCESS_KEY_ID` or `REMOTION_AWS_SECRET_ACCESS_KEY`
|
|
106
127
|
|
|
107
|
-
##
|
|
128
|
+
## Hosted Registration Flow
|
|
108
129
|
|
|
109
|
-
|
|
130
|
+
For a third-party developer, the hosted publish handoff is:
|
|
110
131
|
|
|
111
132
|
1. push the repo changes that contain the target template folder
|
|
112
|
-
2.
|
|
113
|
-
3.
|
|
114
|
-
|
|
133
|
+
2. register the source location if it is new, or update the existing registration if the repo, branch, or template path changed
|
|
134
|
+
3. tell the platform operator which branch head or commit SHA should be imported
|
|
135
|
+
|
|
136
|
+
Registration data should be treated like this:
|
|
137
|
+
|
|
138
|
+
- `repo_url`: the GitHub repo that contains the template code
|
|
139
|
+
- `branch`: the branch Vidfarm should watch for the latest version, usually `main`
|
|
140
|
+
- `template_module_path`: the repo-relative path to the template TypeScript entrypoint, for example `src/vidfarm_template_funnychat/src/template.ts`
|
|
141
|
+
- `commit_sha`: optional and only needed when you want to import a specific commit instead of the current branch head
|
|
142
|
+
|
|
143
|
+
Do not tell third-party developers to run AWS-backed operator commands just to publish template code. If the task is only "make my updated template available to Vidfarm", the correct answer is GitHub push plus source registration metadata.
|
|
144
|
+
|
|
145
|
+
## Template Deploy Cycle
|
|
146
|
+
|
|
147
|
+
If the user explicitly says "template deploy cycle", interpret that as the internal platform-operator sequence:
|
|
148
|
+
|
|
149
|
+
1. import that template commit into the hosted platform as a template source release
|
|
150
|
+
2. approve and activate the new release
|
|
151
|
+
3. run the prod deploy script if platform code changed, or reuse the current prod image for a formal restart if the change was template-only
|
|
115
152
|
|
|
116
153
|
Do not assume "template deploy cycle" means "deploy the entire dirty root repo". If the root worktree has unrelated changes and the release is template-only, prefer reusing the current prod image with:
|
|
117
154
|
|
|
@@ -229,7 +266,7 @@ npx @mevdragon/vidfarm-devcli presign-preview-media \
|
|
|
229
266
|
--directory drafts/preview
|
|
230
267
|
```
|
|
231
268
|
|
|
232
|
-
That command calls the hosted Vidfarm API with the user's `VIDFARM_API_KEY`, mints a presigned PUT URL scoped under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns both the `storage_key` and a public-read `preview_media_url`.
|
|
269
|
+
That command calls the hosted Vidfarm API with the user's `VIDFARM_API_KEY`, mints a presigned PUT URL scoped under `developer/<user_id>/*`, stores each uploaded file under a UUID path segment while preserving the original filename, uploads the file automatically when `--file` is provided, and returns both the `storage_key` and a public-read `preview_media_url`. Public readability for `developer/<user_id>/*` comes from Vidfarm bucket policy, not from public write access, so objects may be read directly by URL but should not be treated as listable or writable without authenticated API access. Agents should prefer that path for hosted preview uploads whenever the input is a local media file path.
|
|
233
270
|
|
|
234
271
|
### 2. Start local runtime
|
|
235
272
|
|
package/dist/src/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { AuthService } from "./services/auth.js";
|
|
|
15
15
|
import { JobsService } from "./services/jobs.js";
|
|
16
16
|
import { StorageService } from "./services/storage.js";
|
|
17
17
|
import { TemplateSourceService } from "./services/template-sources.js";
|
|
18
|
+
import { TemplateSourceAlreadyRegisteredError } from "./services/template-sources.js";
|
|
18
19
|
const auth = new AuthService();
|
|
19
20
|
const jobs = new JobsService();
|
|
20
21
|
const storage = new StorageService();
|
|
@@ -983,7 +984,7 @@ app.post(`${USER_PREFIX}/me/developer/preview-media/presign`, async (c) => {
|
|
|
983
984
|
const storageKey = storage.developerScopedKey(customer.id, directory, randomUUID(), fileName);
|
|
984
985
|
const upload = await storage.createWriteUrl(storageKey, {
|
|
985
986
|
contentType,
|
|
986
|
-
publicRead:
|
|
987
|
+
publicRead: false,
|
|
987
988
|
expiresIn: 3600
|
|
988
989
|
});
|
|
989
990
|
const publicUrl = storage.getPublicUrl(storageKey) ?? buildAbsoluteUrl(c, `/template-media?key=${encodeURIComponent(storageKey)}`);
|
|
@@ -1065,7 +1066,7 @@ app.post(`${TEMPLATES_PREFIX}/sources`, async (c) => {
|
|
|
1065
1066
|
install_command: z.string().min(1).default("npm install"),
|
|
1066
1067
|
build_command: z.string().min(1).default("npm run build")
|
|
1067
1068
|
}).parse(await c.req.json());
|
|
1068
|
-
const
|
|
1069
|
+
const registration = await templateSources.registerSource({
|
|
1069
1070
|
templateId: body.template_id,
|
|
1070
1071
|
slugId: body.slug_id,
|
|
1071
1072
|
repoUrl: body.repo_url,
|
|
@@ -1075,9 +1076,21 @@ app.post(`${TEMPLATES_PREFIX}/sources`, async (c) => {
|
|
|
1075
1076
|
installCommand: body.install_command,
|
|
1076
1077
|
buildCommand: body.build_command
|
|
1077
1078
|
});
|
|
1078
|
-
return c.json({
|
|
1079
|
+
return c.json({
|
|
1080
|
+
source: registration.source,
|
|
1081
|
+
registration_action: registration.action,
|
|
1082
|
+
sync: registration.sync
|
|
1083
|
+
}, registration.action === "created" ? 201 : 200);
|
|
1079
1084
|
}
|
|
1080
1085
|
catch (error) {
|
|
1086
|
+
if (error instanceof TemplateSourceAlreadyRegisteredError) {
|
|
1087
|
+
return c.json({
|
|
1088
|
+
error: error.message,
|
|
1089
|
+
registration_status: error.existingSource.status,
|
|
1090
|
+
existing_source: error.existingSource,
|
|
1091
|
+
conflict_field: error.conflictField
|
|
1092
|
+
}, 409);
|
|
1093
|
+
}
|
|
1081
1094
|
const message = error instanceof Error ? error.message : "Unable to register template source.";
|
|
1082
1095
|
const status = /already exists/i.test(message) ? 409 : 400;
|
|
1083
1096
|
return c.json({ error: message }, status);
|
package/dist/src/cli.js
CHANGED
|
@@ -235,7 +235,7 @@ async function runImportSourceCommand(argv) {
|
|
|
235
235
|
import("./registry.js")
|
|
236
236
|
]);
|
|
237
237
|
const sources = new TemplateSourceService();
|
|
238
|
-
const
|
|
238
|
+
const registration = await sources.registerSource({
|
|
239
239
|
templateId,
|
|
240
240
|
slugId,
|
|
241
241
|
repoUrl,
|
|
@@ -246,7 +246,7 @@ async function runImportSourceCommand(argv) {
|
|
|
246
246
|
buildCommand: parsed.values["build-command"]
|
|
247
247
|
});
|
|
248
248
|
const release = await sources.importRelease({
|
|
249
|
-
sourceId: source.id,
|
|
249
|
+
sourceId: registration.source.id,
|
|
250
250
|
commitSha: parsed.values["commit-sha"] ?? null
|
|
251
251
|
});
|
|
252
252
|
let activated = null;
|
|
@@ -256,7 +256,9 @@ async function runImportSourceCommand(argv) {
|
|
|
256
256
|
activated = result.release;
|
|
257
257
|
}
|
|
258
258
|
console.log(JSON.stringify({
|
|
259
|
-
source,
|
|
259
|
+
source: registration.source,
|
|
260
|
+
registration_action: registration.action,
|
|
261
|
+
sync: registration.sync,
|
|
260
262
|
release,
|
|
261
263
|
activated_release: activated
|
|
262
264
|
}, null, 2));
|
package/dist/src/db.js
CHANGED
|
@@ -845,6 +845,35 @@ export const database = {
|
|
|
845
845
|
});
|
|
846
846
|
return this.getTemplateSourceByTemplateId(record.templateId);
|
|
847
847
|
},
|
|
848
|
+
updateTemplateSource(input) {
|
|
849
|
+
const current = this.getTemplateSource(input.id);
|
|
850
|
+
if (!current) {
|
|
851
|
+
throw new Error("Template source not found.");
|
|
852
|
+
}
|
|
853
|
+
db.prepare(`
|
|
854
|
+
update template_sources
|
|
855
|
+
set repo_url = @repo_url,
|
|
856
|
+
branch = @branch,
|
|
857
|
+
template_module_path = @template_module_path,
|
|
858
|
+
skill_path = @skill_path,
|
|
859
|
+
install_command = @install_command,
|
|
860
|
+
build_command = @build_command,
|
|
861
|
+
status = @status,
|
|
862
|
+
updated_at = @updated_at
|
|
863
|
+
where id = @id
|
|
864
|
+
`).run({
|
|
865
|
+
id: input.id,
|
|
866
|
+
repo_url: input.repoUrl,
|
|
867
|
+
branch: input.branch,
|
|
868
|
+
template_module_path: input.templateModulePath,
|
|
869
|
+
skill_path: input.skillPath,
|
|
870
|
+
install_command: input.installCommand,
|
|
871
|
+
build_command: input.buildCommand,
|
|
872
|
+
status: input.status ?? current.status,
|
|
873
|
+
updated_at: nowIso()
|
|
874
|
+
});
|
|
875
|
+
return this.getTemplateSource(input.id);
|
|
876
|
+
},
|
|
848
877
|
getTemplateSource(id) {
|
|
849
878
|
const row = db.prepare(`select * from template_sources where id = ?`).get(id);
|
|
850
879
|
return row ? mapTemplateSource(row) : null;
|
|
@@ -857,6 +886,15 @@ export const database = {
|
|
|
857
886
|
const row = db.prepare(`select * from template_sources where slug_id = ?`).get(slugId);
|
|
858
887
|
return row ? mapTemplateSource(row) : null;
|
|
859
888
|
},
|
|
889
|
+
getTemplateSourceByLocation(repoUrl, branch, templateModulePath) {
|
|
890
|
+
const row = db.prepare(`
|
|
891
|
+
select *
|
|
892
|
+
from template_sources
|
|
893
|
+
where repo_url = ? and branch = ? and template_module_path = ?
|
|
894
|
+
limit 1
|
|
895
|
+
`).get(repoUrl, branch, templateModulePath);
|
|
896
|
+
return row ? mapTemplateSource(row) : null;
|
|
897
|
+
},
|
|
860
898
|
listTemplateSources() {
|
|
861
899
|
return db.prepare(`select * from template_sources order by template_id asc`).all().map(mapTemplateSource);
|
|
862
900
|
},
|
|
@@ -903,6 +941,16 @@ export const database = {
|
|
|
903
941
|
const row = db.prepare(`select * from template_releases where source_id = ? and commit_sha = ?`).get(sourceId, commitSha);
|
|
904
942
|
return row ? mapTemplateRelease(row) : null;
|
|
905
943
|
},
|
|
944
|
+
getLatestTemplateReleaseForSource(sourceId) {
|
|
945
|
+
const row = db.prepare(`
|
|
946
|
+
select *
|
|
947
|
+
from template_releases
|
|
948
|
+
where source_id = ?
|
|
949
|
+
order by created_at desc
|
|
950
|
+
limit 1
|
|
951
|
+
`).get(sourceId);
|
|
952
|
+
return row ? mapTemplateRelease(row) : null;
|
|
953
|
+
},
|
|
906
954
|
listTemplateReleases(templateId) {
|
|
907
955
|
const rows = templateId
|
|
908
956
|
? db.prepare(`select * from template_releases where template_id = ? order by created_at desc`).all(templateId)
|
|
@@ -10,6 +10,16 @@ import { nowIso } from "../lib/time.js";
|
|
|
10
10
|
import { loadTemplateFromModule } from "./template-loader.js";
|
|
11
11
|
import { TemplateCertificationService } from "./template-certification.js";
|
|
12
12
|
const execFileAsync = promisify(execFile);
|
|
13
|
+
export class TemplateSourceAlreadyRegisteredError extends Error {
|
|
14
|
+
existingSource;
|
|
15
|
+
conflictField;
|
|
16
|
+
constructor(input) {
|
|
17
|
+
super(input.message);
|
|
18
|
+
this.name = "TemplateSourceAlreadyRegisteredError";
|
|
19
|
+
this.existingSource = input.existingSource;
|
|
20
|
+
this.conflictField = input.conflictField;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
export class TemplateSourceService {
|
|
14
24
|
certification = new TemplateCertificationService();
|
|
15
25
|
listSources() {
|
|
@@ -18,28 +28,97 @@ export class TemplateSourceService {
|
|
|
18
28
|
listReleases(templateId) {
|
|
19
29
|
return database.listTemplateReleases(templateId);
|
|
20
30
|
}
|
|
21
|
-
registerSource(input) {
|
|
31
|
+
async registerSource(input) {
|
|
22
32
|
deriveTemplateRootDirFromModulePath(input.templateModulePath);
|
|
33
|
+
const branch = input.branch ?? "production";
|
|
23
34
|
const existingByTemplateId = database.getTemplateSourceByTemplateId(input.templateId);
|
|
24
|
-
if (existingByTemplateId) {
|
|
25
|
-
throw new
|
|
35
|
+
if (existingByTemplateId && existingByTemplateId.slugId !== input.slugId) {
|
|
36
|
+
throw new TemplateSourceAlreadyRegisteredError({
|
|
37
|
+
message: `Template ${input.templateId} is already registered with slug ${existingByTemplateId.slugId}.`,
|
|
38
|
+
existingSource: existingByTemplateId,
|
|
39
|
+
conflictField: "template_id"
|
|
40
|
+
});
|
|
26
41
|
}
|
|
27
42
|
const existingBySlugId = database.getTemplateSourceBySlugId(input.slugId);
|
|
28
|
-
if (existingBySlugId) {
|
|
29
|
-
throw new
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
if (existingBySlugId && existingBySlugId.templateId !== input.templateId) {
|
|
44
|
+
throw new TemplateSourceAlreadyRegisteredError({
|
|
45
|
+
message: `Template slug ${input.slugId} is already registered to template ${existingBySlugId.templateId}.`,
|
|
46
|
+
existingSource: existingBySlugId,
|
|
47
|
+
conflictField: "slug_id"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const existingByLocation = database.getTemplateSourceByLocation(input.repoUrl, branch, input.templateModulePath);
|
|
51
|
+
if (existingByLocation &&
|
|
52
|
+
(existingByLocation.templateId !== input.templateId || existingByLocation.slugId !== input.slugId)) {
|
|
53
|
+
throw new TemplateSourceAlreadyRegisteredError({
|
|
54
|
+
message: `Template source ${input.repoUrl}#${branch}:${input.templateModulePath} is already registered to ${existingByLocation.templateId}/${existingByLocation.slugId}.`,
|
|
55
|
+
existingSource: existingByLocation,
|
|
56
|
+
conflictField: "source_location"
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const existingSource = existingByTemplateId ?? existingBySlugId ?? existingByLocation ?? null;
|
|
60
|
+
const installCommand = input.installCommand ?? "npm install";
|
|
61
|
+
const buildCommand = input.buildCommand ?? "npm run build";
|
|
62
|
+
const skillPath = input.skillPath ?? defaultSkillPathForTemplateModule(input.templateModulePath);
|
|
63
|
+
if (existingSource) {
|
|
64
|
+
const nextSource = existingSource.repoUrl !== input.repoUrl ||
|
|
65
|
+
existingSource.branch !== branch ||
|
|
66
|
+
existingSource.templateModulePath !== input.templateModulePath ||
|
|
67
|
+
existingSource.skillPath !== skillPath ||
|
|
68
|
+
existingSource.installCommand !== installCommand ||
|
|
69
|
+
existingSource.buildCommand !== buildCommand
|
|
70
|
+
? database.updateTemplateSource({
|
|
71
|
+
id: existingSource.id,
|
|
72
|
+
repoUrl: input.repoUrl,
|
|
73
|
+
branch,
|
|
74
|
+
templateModulePath: input.templateModulePath,
|
|
75
|
+
skillPath,
|
|
76
|
+
installCommand,
|
|
77
|
+
buildCommand
|
|
78
|
+
})
|
|
79
|
+
: existingSource;
|
|
80
|
+
return {
|
|
81
|
+
source: nextSource,
|
|
82
|
+
action: nextSource.updatedAt === existingSource.updatedAt ? "unchanged" : "updated",
|
|
83
|
+
sync: await this.getSourceSyncStatus(nextSource.id)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const source = database.createTemplateSource({
|
|
88
|
+
id: createId("tsrc"),
|
|
89
|
+
templateId: input.templateId,
|
|
90
|
+
slugId: input.slugId,
|
|
91
|
+
repoUrl: input.repoUrl,
|
|
92
|
+
branch,
|
|
93
|
+
templateModulePath: input.templateModulePath,
|
|
94
|
+
skillPath,
|
|
95
|
+
installCommand,
|
|
96
|
+
buildCommand,
|
|
97
|
+
status: "active"
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
source,
|
|
101
|
+
action: "created",
|
|
102
|
+
sync: await this.getSourceSyncStatus(source.id)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const existingSource = database.getTemplateSourceByTemplateId(input.templateId) ??
|
|
107
|
+
database.getTemplateSourceBySlugId(input.slugId) ??
|
|
108
|
+
database.getTemplateSourceByLocation(input.repoUrl, branch, input.templateModulePath);
|
|
109
|
+
if (existingSource) {
|
|
110
|
+
throw new TemplateSourceAlreadyRegisteredError({
|
|
111
|
+
message: `Template registration already exists with status ${existingSource.status}.`,
|
|
112
|
+
existingSource,
|
|
113
|
+
conflictField: existingSource.templateId === input.templateId
|
|
114
|
+
? "template_id"
|
|
115
|
+
: existingSource.slugId === input.slugId
|
|
116
|
+
? "slug_id"
|
|
117
|
+
: "source_location"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
43
122
|
}
|
|
44
123
|
async importRelease(input) {
|
|
45
124
|
const source = database.getTemplateSource(input.sourceId);
|
|
@@ -120,6 +199,32 @@ export class TemplateSourceService {
|
|
|
120
199
|
}
|
|
121
200
|
};
|
|
122
201
|
}
|
|
202
|
+
async getSourceSyncStatus(sourceId) {
|
|
203
|
+
const source = database.getTemplateSource(sourceId);
|
|
204
|
+
if (!source) {
|
|
205
|
+
throw new Error("Template source not found.");
|
|
206
|
+
}
|
|
207
|
+
const latestRelease = database.getLatestTemplateReleaseForSource(source.id);
|
|
208
|
+
try {
|
|
209
|
+
const headCommitSha = await this.resolveBranchHead(source.repoUrl, source.branch);
|
|
210
|
+
return {
|
|
211
|
+
headCommitSha,
|
|
212
|
+
latestReleaseCommitSha: latestRelease?.commitSha ?? null,
|
|
213
|
+
importTargetCommitSha: headCommitSha,
|
|
214
|
+
hasUnimportedUpdate: latestRelease ? latestRelease.commitSha !== headCommitSha : true,
|
|
215
|
+
headLookupError: null
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
headCommitSha: null,
|
|
221
|
+
latestReleaseCommitSha: latestRelease?.commitSha ?? null,
|
|
222
|
+
importTargetCommitSha: latestRelease?.commitSha ?? null,
|
|
223
|
+
hasUnimportedUpdate: null,
|
|
224
|
+
headLookupError: error instanceof Error ? error.message : String(error)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
123
228
|
async resolveBranchHead(repoUrl, branch) {
|
|
124
229
|
const { stdout } = await execFileAsync("git", ["ls-remote", repoUrl, branch], { cwd: process.cwd() });
|
|
125
230
|
const line = stdout.trim().split("\n").find(Boolean);
|