@ever-works/github-actions-build-plugin 1.0.2 → 1.0.4
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/dist/index.cjs +439 -17
- package/dist/index.d.cts +142 -5
- package/dist/index.d.ts +142 -5
- package/dist/index.js +440 -18
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,31 @@ function octokitActionsRunsPort(octokit) {
|
|
|
42
42
|
);
|
|
43
43
|
return asArray(data, "workflow_runs");
|
|
44
44
|
},
|
|
45
|
+
async listWorkflowRunsConditional({ repository, workflowFile, perPage, etag }) {
|
|
46
|
+
try {
|
|
47
|
+
const response = await octokit.request(
|
|
48
|
+
"GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs",
|
|
49
|
+
{
|
|
50
|
+
owner: repository.owner,
|
|
51
|
+
repo: repository.repo,
|
|
52
|
+
workflow_id: workflowFile,
|
|
53
|
+
per_page: perPage,
|
|
54
|
+
...etag ? { headers: { "if-none-match": etag } } : {}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
const next = response.headers?.etag;
|
|
58
|
+
return {
|
|
59
|
+
notModified: false,
|
|
60
|
+
...typeof next === "string" ? { etag: next } : {},
|
|
61
|
+
runs: asArray(response.data, "workflow_runs")
|
|
62
|
+
};
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.status === 304) {
|
|
65
|
+
return { notModified: true, ...etag ? { etag } : {}, runs: [] };
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
45
70
|
async getWorkflowRun({ repository, runId }) {
|
|
46
71
|
try {
|
|
47
72
|
const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
|
|
@@ -218,7 +243,39 @@ function observeRun({ run, jobs, mode }) {
|
|
|
218
243
|
}
|
|
219
244
|
var RESULT_ARTIFACT_MAX_ZIP_BYTES = 65536;
|
|
220
245
|
var RESULT_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
221
|
-
var
|
|
246
|
+
var RESULT_SCHEMA_NAME = "ever-works-build-result";
|
|
247
|
+
var RESULT_SCHEMA_VERSION = 1;
|
|
248
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
249
|
+
// T12's first shape.
|
|
250
|
+
"digest",
|
|
251
|
+
"imageRepository",
|
|
252
|
+
"tags",
|
|
253
|
+
"secretCheck",
|
|
254
|
+
"smoke",
|
|
255
|
+
// The generator's versioned shape (`resultStep` in workflow/generator.ts).
|
|
256
|
+
"schema",
|
|
257
|
+
"version",
|
|
258
|
+
"buildId",
|
|
259
|
+
"sha",
|
|
260
|
+
"verification",
|
|
261
|
+
"reusedDigest",
|
|
262
|
+
"localBuild"
|
|
263
|
+
]);
|
|
264
|
+
var VERIFICATION_KEYS = /* @__PURE__ */ new Set(["jobs", "smoke"]);
|
|
265
|
+
var COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/i;
|
|
266
|
+
function normaliseResultDigest(raw) {
|
|
267
|
+
if (typeof raw !== "string") return "";
|
|
268
|
+
const trimmed = raw.trim();
|
|
269
|
+
const at = trimmed.lastIndexOf("@");
|
|
270
|
+
return at >= 0 ? trimmed.slice(at + 1) : trimmed;
|
|
271
|
+
}
|
|
272
|
+
function passRows(value) {
|
|
273
|
+
if (!Array.isArray(value)) return [];
|
|
274
|
+
return value.filter((row) => typeof row === "object" && row !== null).map((row) => ({
|
|
275
|
+
name: String(row.name ?? ""),
|
|
276
|
+
passed: row.passed === true || row.conclusion === "success"
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
222
279
|
function parseResultArtifact(value) {
|
|
223
280
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
224
281
|
return { ok: false, refusal: "malformedJson" };
|
|
@@ -228,22 +285,55 @@ function parseResultArtifact(value) {
|
|
|
228
285
|
if (unknown.length > 0) {
|
|
229
286
|
return { ok: false, refusal: "unknownKeys" };
|
|
230
287
|
}
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
288
|
+
const versioned = "schema" in bag || "version" in bag;
|
|
289
|
+
if (versioned && (bag.schema !== RESULT_SCHEMA_NAME || bag.version !== RESULT_SCHEMA_VERSION)) {
|
|
290
|
+
return { ok: false, refusal: "unsupportedSchema" };
|
|
291
|
+
}
|
|
292
|
+
let verification = null;
|
|
293
|
+
if (bag.verification !== void 0) {
|
|
294
|
+
if (typeof bag.verification !== "object" || bag.verification === null || Array.isArray(bag.verification)) {
|
|
295
|
+
return { ok: false, refusal: "malformedJson" };
|
|
296
|
+
}
|
|
297
|
+
verification = bag.verification;
|
|
298
|
+
if (Object.keys(verification).some((key) => !VERIFICATION_KEYS.has(key))) {
|
|
299
|
+
return { ok: false, refusal: "unknownKeys" };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const digest = normaliseResultDigest(bag.digest);
|
|
303
|
+
if (digest.length > 0 || !versioned) {
|
|
304
|
+
if (!RESULT_DIGEST_PATTERN.test(digest)) {
|
|
305
|
+
return { ok: false, refusal: "badDigest" };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const reusedDigest = normaliseResultDigest(bag.reusedDigest);
|
|
309
|
+
if (reusedDigest.length > 0 && !RESULT_DIGEST_PATTERN.test(reusedDigest)) {
|
|
233
310
|
return { ok: false, refusal: "badDigest" };
|
|
234
311
|
}
|
|
235
|
-
const result = {
|
|
312
|
+
const result = {};
|
|
313
|
+
if (digest.length > 0) result.digest = digest;
|
|
314
|
+
if (versioned) result.schemaVersion = RESULT_SCHEMA_VERSION;
|
|
236
315
|
if (typeof bag.imageRepository === "string" && bag.imageRepository.trim().length > 0) {
|
|
237
316
|
result.imageRepository = bag.imageRepository.trim();
|
|
238
317
|
}
|
|
239
318
|
if (Array.isArray(bag.tags)) {
|
|
240
|
-
result.tags = bag.tags.filter((tag) => typeof tag === "string");
|
|
319
|
+
result.tags = bag.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
|
241
320
|
}
|
|
242
321
|
if (bag.secretCheck === "passed" || bag.secretCheck === "failed" || bag.secretCheck === "not_needed") {
|
|
243
322
|
result.secretCheck = bag.secretCheck;
|
|
244
323
|
}
|
|
245
|
-
if (
|
|
246
|
-
result.
|
|
324
|
+
if (typeof bag.buildId === "string" && bag.buildId.trim().length > 0) {
|
|
325
|
+
result.buildId = bag.buildId.trim();
|
|
326
|
+
}
|
|
327
|
+
if (typeof bag.sha === "string" && COMMIT_SHA_PATTERN.test(bag.sha.trim())) {
|
|
328
|
+
result.sha = bag.sha.trim().toLowerCase();
|
|
329
|
+
}
|
|
330
|
+
if (reusedDigest.length > 0) result.reusedDigest = reusedDigest;
|
|
331
|
+
if (typeof bag.localBuild === "boolean") result.localBuild = bag.localBuild;
|
|
332
|
+
if (Array.isArray(bag.smoke) || Array.isArray(verification?.smoke)) {
|
|
333
|
+
result.smoke = [...passRows(bag.smoke), ...passRows(verification?.smoke)];
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(verification?.jobs)) {
|
|
336
|
+
result.verificationJobs = passRows(verification?.jobs);
|
|
247
337
|
}
|
|
248
338
|
return { ok: true, result };
|
|
249
339
|
}
|
|
@@ -285,7 +375,11 @@ async function readResultArtifact(port, input) {
|
|
|
285
375
|
} catch {
|
|
286
376
|
return { ok: false, refusal: "malformedJson" };
|
|
287
377
|
}
|
|
288
|
-
|
|
378
|
+
const parsed = parseResultArtifact(decoded);
|
|
379
|
+
if (parsed.ok && input.expectBuildId && parsed.result.buildId && parsed.result.buildId !== input.expectBuildId) {
|
|
380
|
+
return { ok: false, refusal: "buildMismatch" };
|
|
381
|
+
}
|
|
382
|
+
return parsed;
|
|
289
383
|
}
|
|
290
384
|
|
|
291
385
|
// src/runs/failure-classifier.ts
|
|
@@ -1291,7 +1385,7 @@ function resultStep(job, secretCheckPresent) {
|
|
|
1291
1385
|
`jq -n --arg buildId "\${{ inputs.ew_build_id }}" --arg sha "$EW_SHA" \\`,
|
|
1292
1386
|
` --arg imageRepository "$EW_IMAGE" --arg digest "$digest" --arg secretCheck "$secret_check" \\`,
|
|
1293
1387
|
` --arg tag "$EW_IMAGE:sha-$EW_SHA" \\`,
|
|
1294
|
-
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository
|
|
1388
|
+
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository,`,
|
|
1295
1389
|
` digest:$digest,tags:[$tag],secretCheck:$secretCheck,verification:{jobs:[],smoke:[]}}' > ${contracts.APP_BUILD_RESULT_ARTIFACT_FILE}`
|
|
1296
1390
|
] : [
|
|
1297
1391
|
"set -euo pipefail",
|
|
@@ -1301,7 +1395,7 @@ function resultStep(job, secretCheckPresent) {
|
|
|
1301
1395
|
`jq -n --arg buildId "\${{ inputs.ew_build_id }}" --arg sha "$EW_SHA" \\`,
|
|
1302
1396
|
` --arg imageRepository "$EW_IMAGE" --arg reusedDigest "$reused_digest" \\`,
|
|
1303
1397
|
` --argjson localBuild "$local_build" \\`,
|
|
1304
|
-
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository
|
|
1398
|
+
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository,`,
|
|
1305
1399
|
` reusedDigest:$reusedDigest,localBuild:$localBuild,verification:{jobs:[],smoke:[]}}' > ${contracts.APP_BUILD_RESULT_ARTIFACT_FILE}`
|
|
1306
1400
|
];
|
|
1307
1401
|
return [" - name: Write result", " if: always()", " run: |", ...scalarBlock(script, 10)];
|
|
@@ -1375,7 +1469,8 @@ async function readBranchHead(writer, branch) {
|
|
|
1375
1469
|
function repositoryWriteErrorCode(error) {
|
|
1376
1470
|
const candidate = error;
|
|
1377
1471
|
for (const value of [candidate?.code, candidate?.reason]) {
|
|
1378
|
-
if (value === "nonFastForward" || value === "refRejectedByRule" || value === "pullRequestExists"
|
|
1472
|
+
if (value === "nonFastForward" || value === "refRejectedByRule" || value === "pullRequestExists" || value === "workflowPermissionMissing")
|
|
1473
|
+
return value;
|
|
1379
1474
|
}
|
|
1380
1475
|
return null;
|
|
1381
1476
|
}
|
|
@@ -1417,14 +1512,36 @@ async function writeWorkflow(input, writer, protection) {
|
|
|
1417
1512
|
const removal = input.content === "";
|
|
1418
1513
|
if (removal) {
|
|
1419
1514
|
if (!existsOnTrackedBranch) return { state: "unchanged", storedWorkflowSha256: null };
|
|
1420
|
-
return
|
|
1515
|
+
return withWorkflowPermission(
|
|
1516
|
+
() => pullRequestDelivery(input, writer, path, contentSha256, handEdited, REMOVAL_COPY)
|
|
1517
|
+
);
|
|
1421
1518
|
}
|
|
1422
1519
|
const verdict = createdByAppWork && !handEdited && protection ? await isBranchProtected(protection, { owner, repo, branch: trackedBranch }) : { protected: false};
|
|
1423
1520
|
const pullRequestPath = !createdByAppWork || handEdited || verdict.protected;
|
|
1424
1521
|
if (pullRequestPath) {
|
|
1425
|
-
return pullRequestDelivery(input, writer, path, contentSha256, handEdited);
|
|
1522
|
+
return withWorkflowPermission(() => pullRequestDelivery(input, writer, path, contentSha256, handEdited));
|
|
1523
|
+
}
|
|
1524
|
+
return withWorkflowPermission(() => directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch));
|
|
1525
|
+
}
|
|
1526
|
+
async function withWorkflowPermission(deliver) {
|
|
1527
|
+
try {
|
|
1528
|
+
return await deliver();
|
|
1529
|
+
} catch (error) {
|
|
1530
|
+
if (repositoryWriteErrorCode(error) !== "workflowPermissionMissing") throw error;
|
|
1531
|
+
const facts = error;
|
|
1532
|
+
return {
|
|
1533
|
+
state: "blocked",
|
|
1534
|
+
storedWorkflowSha256: null,
|
|
1535
|
+
blocked: {
|
|
1536
|
+
reason: "workflowPermissionMissing",
|
|
1537
|
+
detail: {
|
|
1538
|
+
cause: "workflowPermissionMissing",
|
|
1539
|
+
credential: typeof facts.credential === "string" ? facts.credential : "unknown",
|
|
1540
|
+
missing: typeof facts.missing === "string" ? facts.missing : "workflow"
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1426
1544
|
}
|
|
1427
|
-
return directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch);
|
|
1428
1545
|
}
|
|
1429
1546
|
async function directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch) {
|
|
1430
1547
|
const { trackedBranch } = input.repository;
|
|
@@ -1669,6 +1786,234 @@ function createBuildValueSecretSync(dependencies) {
|
|
|
1669
1786
|
};
|
|
1670
1787
|
}
|
|
1671
1788
|
|
|
1789
|
+
// src/repo/octokit-repository-writer.ts
|
|
1790
|
+
var WORKFLOWS_DIRECTORY = ".github/workflows/";
|
|
1791
|
+
var RepositoryWriteError = class extends Error {
|
|
1792
|
+
constructor(code, message, cause, credential, missing) {
|
|
1793
|
+
super(message);
|
|
1794
|
+
this.code = code;
|
|
1795
|
+
this.cause = cause;
|
|
1796
|
+
this.credential = credential;
|
|
1797
|
+
this.missing = missing;
|
|
1798
|
+
this.name = "RepositoryWriteError";
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
function workflowPermissionMissingError(credential, cause) {
|
|
1802
|
+
const app = credential === "githubApp";
|
|
1803
|
+
return new RepositoryWriteError(
|
|
1804
|
+
"workflowPermissionMissing",
|
|
1805
|
+
app ? 'The GitHub App installation cannot write workflow files: grant it "Workflows: Read and write".' : 'The GitHub connection cannot write workflow files: reconnect GitHub and grant the "workflow" scope.',
|
|
1806
|
+
cause,
|
|
1807
|
+
credential,
|
|
1808
|
+
app ? "workflows:write" : "workflow"
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
var FULL_SHA2 = /^[0-9a-f]{40}$/i;
|
|
1812
|
+
function statusOf(error) {
|
|
1813
|
+
const status = error?.status;
|
|
1814
|
+
return typeof status === "number" ? status : void 0;
|
|
1815
|
+
}
|
|
1816
|
+
function messageOf(error) {
|
|
1817
|
+
const candidate = error;
|
|
1818
|
+
const parts = [];
|
|
1819
|
+
if (typeof candidate?.message === "string") parts.push(candidate.message);
|
|
1820
|
+
const data = candidate?.response?.data;
|
|
1821
|
+
if (typeof data?.message === "string") parts.push(data.message);
|
|
1822
|
+
if (Array.isArray(data?.errors)) {
|
|
1823
|
+
for (const entry of data.errors) {
|
|
1824
|
+
const message = entry?.message;
|
|
1825
|
+
if (typeof message === "string") parts.push(message);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return parts.join(" | ");
|
|
1829
|
+
}
|
|
1830
|
+
var RULE_REFUSAL = /protected branch|ruleset|rule violation|required status|required review|push declined|cannot be updated/i;
|
|
1831
|
+
var WORKFLOW_REFUSAL = /workflow[^|]*\b(scope|permission)|\b(scope|permission)[^|]*workflow/i;
|
|
1832
|
+
var INTEGRATION_REFUSAL = /resource not accessible by integration/i;
|
|
1833
|
+
function scopesFromHeader(headers) {
|
|
1834
|
+
const header = headers?.["x-oauth-scopes"];
|
|
1835
|
+
if (typeof header !== "string") return null;
|
|
1836
|
+
return header.split(",").map((scope) => scope.trim()).filter(Boolean);
|
|
1837
|
+
}
|
|
1838
|
+
function octokitRepositoryWriter(octokit, repository, credential = {}) {
|
|
1839
|
+
const base = { owner: repository.owner, repo: repository.repo };
|
|
1840
|
+
const kind = credential.credential ?? "unknown";
|
|
1841
|
+
async function headOf(ref) {
|
|
1842
|
+
if (FULL_SHA2.test(ref)) return ref;
|
|
1843
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/git/ref/{ref}", {
|
|
1844
|
+
...base,
|
|
1845
|
+
ref: `heads/${ref}`
|
|
1846
|
+
});
|
|
1847
|
+
const sha = data?.object?.sha;
|
|
1848
|
+
if (typeof sha !== "string") throw new Error(`github-actions-build: no head commit for ${ref}`);
|
|
1849
|
+
return sha;
|
|
1850
|
+
}
|
|
1851
|
+
async function assertCanWriteWorkflows() {
|
|
1852
|
+
if (kind === "githubApp") return;
|
|
1853
|
+
let scopes = credential.scopes ?? null;
|
|
1854
|
+
if (!scopes) {
|
|
1855
|
+
try {
|
|
1856
|
+
scopes = scopesFromHeader((await octokit.request("GET /user")).headers);
|
|
1857
|
+
} catch {
|
|
1858
|
+
scopes = null;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (scopes && !scopes.includes("workflow")) {
|
|
1862
|
+
throw workflowPermissionMissingError(kind === "pat" ? "pat" : "oauth");
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function translateWorkflowRefusal(error, touchesWorkflows) {
|
|
1866
|
+
if (!touchesWorkflows || error instanceof RepositoryWriteError) return error;
|
|
1867
|
+
const message = messageOf(error);
|
|
1868
|
+
if (WORKFLOW_REFUSAL.test(message)) {
|
|
1869
|
+
return workflowPermissionMissingError(/github app|integration/i.test(message) ? "githubApp" : kind, error);
|
|
1870
|
+
}
|
|
1871
|
+
return error;
|
|
1872
|
+
}
|
|
1873
|
+
async function commit(input, touchesWorkflows) {
|
|
1874
|
+
const { data: baseCommit } = await octokit.request("GET /repos/{owner}/{repo}/git/commits/{commit_sha}", {
|
|
1875
|
+
...base,
|
|
1876
|
+
commit_sha: input.baseSha
|
|
1877
|
+
});
|
|
1878
|
+
const baseTree = baseCommit?.tree?.sha;
|
|
1879
|
+
if (typeof baseTree !== "string") {
|
|
1880
|
+
throw new Error(`github-actions-build: commit ${input.baseSha} has no tree`);
|
|
1881
|
+
}
|
|
1882
|
+
const tree = [];
|
|
1883
|
+
for (const file of input.files) {
|
|
1884
|
+
const { data: blob } = await octokit.request("POST /repos/{owner}/{repo}/git/blobs", {
|
|
1885
|
+
...base,
|
|
1886
|
+
content: file.content,
|
|
1887
|
+
encoding: file.encoding === "base64" ? "base64" : "utf-8"
|
|
1888
|
+
});
|
|
1889
|
+
const sha = blob?.sha;
|
|
1890
|
+
if (typeof sha !== "string") throw new Error(`github-actions-build: no blob sha for ${file.path}`);
|
|
1891
|
+
tree.push({ path: file.path, mode: "100644", type: "blob", sha });
|
|
1892
|
+
}
|
|
1893
|
+
const { data: newTree } = await octokit.request("POST /repos/{owner}/{repo}/git/trees", {
|
|
1894
|
+
...base,
|
|
1895
|
+
base_tree: baseTree,
|
|
1896
|
+
tree
|
|
1897
|
+
});
|
|
1898
|
+
const { data: created } = await octokit.request("POST /repos/{owner}/{repo}/git/commits", {
|
|
1899
|
+
...base,
|
|
1900
|
+
message: input.message,
|
|
1901
|
+
tree: newTree.sha,
|
|
1902
|
+
parents: [input.baseSha]
|
|
1903
|
+
});
|
|
1904
|
+
const commitSha = created.sha;
|
|
1905
|
+
try {
|
|
1906
|
+
await octokit.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", {
|
|
1907
|
+
...base,
|
|
1908
|
+
ref: `heads/${input.branch}`,
|
|
1909
|
+
sha: commitSha,
|
|
1910
|
+
// Never a force-push: a moved branch is `nonFastForward`.
|
|
1911
|
+
force: false
|
|
1912
|
+
});
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
const status = statusOf(error);
|
|
1915
|
+
const message = messageOf(error);
|
|
1916
|
+
if (status === 422 && /fast[ -]?forward/i.test(message)) {
|
|
1917
|
+
throw new RepositoryWriteError(
|
|
1918
|
+
"nonFastForward",
|
|
1919
|
+
`${input.branch} moved; retry from its new head`,
|
|
1920
|
+
error
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
if ((status === 409 || status === 422 || status === 403) && RULE_REFUSAL.test(message)) {
|
|
1924
|
+
throw new RepositoryWriteError("refRejectedByRule", `${input.branch} refused the update`, error);
|
|
1925
|
+
}
|
|
1926
|
+
if (touchesWorkflows && status === 403 && INTEGRATION_REFUSAL.test(message)) {
|
|
1927
|
+
throw workflowPermissionMissingError("githubApp", error);
|
|
1928
|
+
}
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1931
|
+
return { commitSha };
|
|
1932
|
+
}
|
|
1933
|
+
return {
|
|
1934
|
+
async getFileContent(path, ref) {
|
|
1935
|
+
try {
|
|
1936
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/contents/{path}", {
|
|
1937
|
+
...base,
|
|
1938
|
+
path,
|
|
1939
|
+
...ref ?? repository.trackedBranch ? { ref: ref ?? repository.trackedBranch } : {}
|
|
1940
|
+
});
|
|
1941
|
+
const file = data;
|
|
1942
|
+
if (Array.isArray(data) || file?.type !== "file" || typeof file.content !== "string") return null;
|
|
1943
|
+
return {
|
|
1944
|
+
content: file.content,
|
|
1945
|
+
encoding: typeof file.encoding === "string" ? file.encoding : "base64"
|
|
1946
|
+
};
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
if (statusOf(error) === 404) return null;
|
|
1949
|
+
throw error;
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
async commitFiles(input) {
|
|
1953
|
+
const touchesWorkflows = input.files.some((file) => file.path.startsWith(WORKFLOWS_DIRECTORY));
|
|
1954
|
+
if (touchesWorkflows) await assertCanWriteWorkflows();
|
|
1955
|
+
try {
|
|
1956
|
+
return await commit(input, touchesWorkflows);
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
throw translateWorkflowRefusal(error, touchesWorkflows);
|
|
1959
|
+
}
|
|
1960
|
+
},
|
|
1961
|
+
async createBranch(name, fromRef) {
|
|
1962
|
+
try {
|
|
1963
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/branches/{branch}", {
|
|
1964
|
+
...base,
|
|
1965
|
+
branch: name
|
|
1966
|
+
});
|
|
1967
|
+
const branch = data;
|
|
1968
|
+
return {
|
|
1969
|
+
name: branch.name ?? name,
|
|
1970
|
+
commit: branch.commit?.sha ?? "",
|
|
1971
|
+
isDefault: false,
|
|
1972
|
+
isProtected: branch.protected === true
|
|
1973
|
+
};
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
if (statusOf(error) !== 404) throw error;
|
|
1976
|
+
}
|
|
1977
|
+
const sha = await headOf(fromRef);
|
|
1978
|
+
await octokit.request("POST /repos/{owner}/{repo}/git/refs", {
|
|
1979
|
+
...base,
|
|
1980
|
+
ref: `refs/heads/${name}`,
|
|
1981
|
+
sha
|
|
1982
|
+
});
|
|
1983
|
+
return { name, commit: sha, isDefault: false };
|
|
1984
|
+
},
|
|
1985
|
+
async createPullRequest(options) {
|
|
1986
|
+
try {
|
|
1987
|
+
const { data } = await octokit.request("POST /repos/{owner}/{repo}/pulls", {
|
|
1988
|
+
...base,
|
|
1989
|
+
title: options.title,
|
|
1990
|
+
head: options.headOwner ? `${options.headOwner}:${options.head}` : options.head,
|
|
1991
|
+
base: options.base,
|
|
1992
|
+
...options.body ? { body: options.body } : {},
|
|
1993
|
+
...options.draft ? { draft: true } : {}
|
|
1994
|
+
});
|
|
1995
|
+
const pr = data;
|
|
1996
|
+
return {
|
|
1997
|
+
number: pr.number,
|
|
1998
|
+
title: pr.title ?? options.title,
|
|
1999
|
+
state: pr.state === "closed" ? "closed" : "open",
|
|
2000
|
+
head: options.head,
|
|
2001
|
+
base: options.base,
|
|
2002
|
+
url: pr.html_url ?? "",
|
|
2003
|
+
createdAt: pr.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2004
|
+
updatedAt: pr.updated_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2005
|
+
...pr.body ? { body: pr.body } : {}
|
|
2006
|
+
};
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
if (statusOf(error) === 422 && /pull request already exists/i.test(messageOf(error))) {
|
|
2009
|
+
throw new RepositoryWriteError("pullRequestExists", "an open pull request already exists", error);
|
|
2010
|
+
}
|
|
2011
|
+
throw error;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1672
2017
|
// src/settings.schema.ts
|
|
1673
2018
|
var LARGER_RUNNER_LABEL_PATTERN = "^[A-Za-z0-9._-]{1,64}$";
|
|
1674
2019
|
var LARGER_RUNNER_MEMORY_MIN_GIB = 8;
|
|
@@ -1960,7 +2305,9 @@ var GitHubActionsBuildPlugin = class {
|
|
|
1960
2305
|
buildInputsHash: contracts.computeBuildInputsHash(input.values ?? []),
|
|
1961
2306
|
blocked: {
|
|
1962
2307
|
reason: written.blocked?.reason ?? "workflowWriteFailed",
|
|
1963
|
-
|
|
2308
|
+
// The whole detail: `workflowPermissionMissing` carries which credential
|
|
2309
|
+
// and which permission, which is what the member's fix depends on.
|
|
2310
|
+
detail: { ...written.blocked?.detail ?? { cause: "unknown" } }
|
|
1964
2311
|
}
|
|
1965
2312
|
};
|
|
1966
2313
|
}
|
|
@@ -2088,13 +2435,16 @@ var GitHubActionsBuildPlugin = class {
|
|
|
2088
2435
|
const jobs = await port.listRunJobs({ repository, runId });
|
|
2089
2436
|
const snapshot = observeRun({ run, jobs, mode: "build" });
|
|
2090
2437
|
const finished = snapshot.status !== "queued" && snapshot.status !== "running";
|
|
2091
|
-
const artifact = finished ? await readResultArtifact(port, { repository, runId }) : null;
|
|
2438
|
+
const artifact = finished ? await readResultArtifact(port, { repository, runId, expectBuildId: ref.buildId }) : null;
|
|
2092
2439
|
const result = artifact?.ok ? artifact.result : null;
|
|
2093
2440
|
const failure = snapshot.status === "failed" ? await this.classifyRunFailure({ port, repository, run, jobs, snapshot, result, redact }) : null;
|
|
2094
2441
|
const pushLogDigest = result && snapshot.status === "succeeded" && isPushingTrigger(snapshot.trigger) ? await this.readPushLogDigest({ port, repository, jobs, sha: snapshot.commitSha }) : void 0;
|
|
2095
2442
|
return {
|
|
2096
2443
|
...snapshot,
|
|
2097
|
-
|
|
2444
|
+
// A versioned result with no digest (a failed build, a pull request, a
|
|
2445
|
+
// verify job) still reports its secret check — it just claims no image.
|
|
2446
|
+
...result?.secretCheck && !result.digest ? { secretCheck: result.secretCheck } : {},
|
|
2447
|
+
...result?.digest ? {
|
|
2098
2448
|
// `confirmed: false` without exception. This plugin reads the digest
|
|
2099
2449
|
// the member's own CI claimed; confirming it against the registry is
|
|
2100
2450
|
// `checkImageAccess` (T14), compared by `AppBuildsService.finalize`,
|
|
@@ -2249,6 +2599,78 @@ var GitHubActionsBuildPlugin = class {
|
|
|
2249
2599
|
registryFetch() {
|
|
2250
2600
|
return (url, init) => fetch(url, init);
|
|
2251
2601
|
}
|
|
2602
|
+
/**
|
|
2603
|
+
* APW-05 T21a / T12 remainder — run discovery (plan §7.4a, `APW05-G01`).
|
|
2604
|
+
*
|
|
2605
|
+
* The newest runs of the build workflow, so a push or pull-request Build is
|
|
2606
|
+
* recorded even when no `workflow_run` delivery reaches the platform (a
|
|
2607
|
+
* personal fork connected through OAuth has no App installation to deliver
|
|
2608
|
+
* from). `If-None-Match` with the stored `etag` makes an idle repository cost
|
|
2609
|
+
* a 304 — which GitHub does not count against the rate limit.
|
|
2610
|
+
*
|
|
2611
|
+
* The mapping is `githubRunToBuildRunRef` from contracts, the SAME function
|
|
2612
|
+
* the webhook consumer uses, so a polled run and a delivered run are the same
|
|
2613
|
+
* `BuildRunRef` and converge on one Build row.
|
|
2614
|
+
*/
|
|
2615
|
+
async listRecentRuns(input, auth) {
|
|
2616
|
+
const port = this.actionsPort(auth);
|
|
2617
|
+
const repository = { owner: input.repository.owner, repo: input.repository.name };
|
|
2618
|
+
const workflowFile = input.workflowPath.split("/").pop() || WORKFLOW_FILE_NAME;
|
|
2619
|
+
const perPage = Math.max(1, Math.min(contracts.APP_BUILD_RUN_DISCOVERY_PER_PAGE, Math.floor(input.perPage) || 1));
|
|
2620
|
+
const page = port.listWorkflowRunsConditional ? await port.listWorkflowRunsConditional({
|
|
2621
|
+
repository,
|
|
2622
|
+
workflowFile,
|
|
2623
|
+
perPage,
|
|
2624
|
+
...input.etag ? { etag: input.etag } : {}
|
|
2625
|
+
}) : { notModified: false, runs: await port.listWorkflowRuns({ repository, workflowFile, perPage }) };
|
|
2626
|
+
if (page.notModified) {
|
|
2627
|
+
return { notModified: true, ...page.etag ? { etag: page.etag } : {}, runs: [] };
|
|
2628
|
+
}
|
|
2629
|
+
const runs = page.runs.map((run) => contracts.githubRunToBuildRunRef(run)).filter((run) => run !== null);
|
|
2630
|
+
return { notModified: false, ...page.etag ? { etag: page.etag } : {}, runs };
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* APW-05 T16 remainder — the clone-free writer `prepareRepository` delivers the
|
|
2634
|
+
* workflow through, over this plugin's own Octokit (see
|
|
2635
|
+
* `repo/octokit-repository-writer.ts` on why the plugin supplies it).
|
|
2636
|
+
*/
|
|
2637
|
+
repositoryWriter(repository, auth) {
|
|
2638
|
+
return octokitRepositoryWriter(this.octokitFor(auth), repository, {
|
|
2639
|
+
credential: auth.credential,
|
|
2640
|
+
scopes: auth.scopes
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Plan §5 — a Rebuild's `commitSha` must be reachable from the tracked branch
|
|
2645
|
+
* (2026-10-17). `GET …/compare/{sha}...{branch}`: `ahead` or `identical` means
|
|
2646
|
+
* the commit is in the branch's history; `behind` / `diverged` means it is not
|
|
2647
|
+
* (a commit only a fork's network carries is `diverged`), and 404/422 means
|
|
2648
|
+
* GitHub does not know it here. Any other failure is a throw.
|
|
2649
|
+
*/
|
|
2650
|
+
async isCommitReachable(input, auth) {
|
|
2651
|
+
try {
|
|
2652
|
+
const { data } = await this.octokitFor(auth).request("GET /repos/{owner}/{repo}/compare/{basehead}", {
|
|
2653
|
+
owner: input.repository.owner,
|
|
2654
|
+
repo: input.repository.repo,
|
|
2655
|
+
basehead: `${input.sha}...${input.branch}`,
|
|
2656
|
+
per_page: 1
|
|
2657
|
+
});
|
|
2658
|
+
const status = data?.status;
|
|
2659
|
+
return status === "ahead" || status === "identical";
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
const status = error?.status;
|
|
2662
|
+
if (status === 404 || status === 422) return false;
|
|
2663
|
+
throw error;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
/** §4.10 — the per-run prompted-value secret's removal (T12's `secret-sync.ts`, now on the contract). */
|
|
2667
|
+
async deleteVerifyPromptedSecret(repository, auth) {
|
|
2668
|
+
return createBuildValueSecretSync({ port: this.secretPort(auth, repository) }).deleteVerifyPromptedSecret();
|
|
2669
|
+
}
|
|
2670
|
+
/** The Octokit client for one call — a seam a spec overrides, like {@link actionsPort}. */
|
|
2671
|
+
octokitFor(auth) {
|
|
2672
|
+
return new octokit.Octokit({ auth: auth.token });
|
|
2673
|
+
}
|
|
2252
2674
|
/** APW-05 T12 — the run's own page, which is where a member reads its logs. */
|
|
2253
2675
|
async getLogsUrl(ref, auth) {
|
|
2254
2676
|
const port = this.actionsPort(auth);
|