@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { APP_BUILD_WORKFLOW_PATH, APP_BUILD_RESTRICTED_VALUE_LITERAL, APP_BUILD_CHECK_NAME_PREFIX, sha256Hex, APP_BUILD_BRANCH_SLUG_MAX_CHARS, APP_BUILD_IMAGE_NAME, APP_BUILD_SECRET_PREFIX, APP_BUILD_CHECKS_MAX, APP_BUILD_VERIFY_PLAN_INPUT, resolveBuildServiceKind, resolveBuildServiceEnv, evaluateBuildServicePort, BUILD_SERVICE_DEFAULTS, APP_BUILD_VERIFY_TIMEOUT_MINUTES, APP_BUILD_RESULT_ARTIFACT_RETENTION_DAYS, APP_BUILD_RESULT_ARTIFACT_NAME, APP_BUILD_RESULT_ARTIFACT_FILE,
|
|
1
|
+
import { APP_BUILD_WORKFLOW_PATH, APP_BUILD_RESTRICTED_VALUE_LITERAL, APP_BUILD_CHECK_NAME_PREFIX, sha256Hex, APP_BUILD_BRANCH_SLUG_MAX_CHARS, APP_BUILD_IMAGE_NAME, APP_BUILD_SECRET_PREFIX, APP_BUILD_CHECKS_MAX, APP_BUILD_VERIFY_PLAN_INPUT, resolveBuildServiceKind, resolveBuildServiceEnv, evaluateBuildServicePort, BUILD_SERVICE_DEFAULTS, APP_BUILD_VERIFY_TIMEOUT_MINUTES, APP_BUILD_RESULT_ARTIFACT_RETENTION_DAYS, APP_BUILD_RESULT_ARTIFACT_NAME, APP_BUILD_RESULT_ARTIFACT_FILE, APP_BUILD_VERIFY_PROMPTED_SECRET, APP_BUILD_SECRET_MAX_BYTES, computeBuildInputsHash, APP_BUILD_MAX_VALUES, APP_BUILD_RUN_DISCOVERY_PER_PAGE, githubRunToBuildRunRef, APP_BUILD_DEPLOYABLE_TRIGGERS, APP_BUILD_CHECKS_MAX_PARALLEL, APP_BUILD_RESULT_MAX_BYTES, APP_BUILD_WORKFLOW_BRANCH, APP_BUILD_RUNNERS, appBuildRunnerCapacity, APP_BUILD_RUNNER_HEADROOM_GIB, APP_BUILD_CHECKS_JOB_ID } from '@ever-works/contracts';
|
|
2
2
|
import { Octokit } from 'octokit';
|
|
3
3
|
import { unzipSync } from 'fflate';
|
|
4
4
|
import { createRequire } from 'module';
|
|
@@ -37,6 +37,31 @@ function octokitActionsRunsPort(octokit) {
|
|
|
37
37
|
);
|
|
38
38
|
return asArray(data, "workflow_runs");
|
|
39
39
|
},
|
|
40
|
+
async listWorkflowRunsConditional({ repository, workflowFile, perPage, etag }) {
|
|
41
|
+
try {
|
|
42
|
+
const response = await octokit.request(
|
|
43
|
+
"GET /repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs",
|
|
44
|
+
{
|
|
45
|
+
owner: repository.owner,
|
|
46
|
+
repo: repository.repo,
|
|
47
|
+
workflow_id: workflowFile,
|
|
48
|
+
per_page: perPage,
|
|
49
|
+
...etag ? { headers: { "if-none-match": etag } } : {}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
const next = response.headers?.etag;
|
|
53
|
+
return {
|
|
54
|
+
notModified: false,
|
|
55
|
+
...typeof next === "string" ? { etag: next } : {},
|
|
56
|
+
runs: asArray(response.data, "workflow_runs")
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.status === 304) {
|
|
60
|
+
return { notModified: true, ...etag ? { etag } : {}, runs: [] };
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
40
65
|
async getWorkflowRun({ repository, runId }) {
|
|
41
66
|
try {
|
|
42
67
|
const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
|
|
@@ -213,7 +238,39 @@ function observeRun({ run, jobs, mode }) {
|
|
|
213
238
|
}
|
|
214
239
|
var RESULT_ARTIFACT_MAX_ZIP_BYTES = 65536;
|
|
215
240
|
var RESULT_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
216
|
-
var
|
|
241
|
+
var RESULT_SCHEMA_NAME = "ever-works-build-result";
|
|
242
|
+
var RESULT_SCHEMA_VERSION = 1;
|
|
243
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
244
|
+
// T12's first shape.
|
|
245
|
+
"digest",
|
|
246
|
+
"imageRepository",
|
|
247
|
+
"tags",
|
|
248
|
+
"secretCheck",
|
|
249
|
+
"smoke",
|
|
250
|
+
// The generator's versioned shape (`resultStep` in workflow/generator.ts).
|
|
251
|
+
"schema",
|
|
252
|
+
"version",
|
|
253
|
+
"buildId",
|
|
254
|
+
"sha",
|
|
255
|
+
"verification",
|
|
256
|
+
"reusedDigest",
|
|
257
|
+
"localBuild"
|
|
258
|
+
]);
|
|
259
|
+
var VERIFICATION_KEYS = /* @__PURE__ */ new Set(["jobs", "smoke"]);
|
|
260
|
+
var COMMIT_SHA_PATTERN = /^[a-f0-9]{40}$/i;
|
|
261
|
+
function normaliseResultDigest(raw) {
|
|
262
|
+
if (typeof raw !== "string") return "";
|
|
263
|
+
const trimmed = raw.trim();
|
|
264
|
+
const at = trimmed.lastIndexOf("@");
|
|
265
|
+
return at >= 0 ? trimmed.slice(at + 1) : trimmed;
|
|
266
|
+
}
|
|
267
|
+
function passRows(value) {
|
|
268
|
+
if (!Array.isArray(value)) return [];
|
|
269
|
+
return value.filter((row) => typeof row === "object" && row !== null).map((row) => ({
|
|
270
|
+
name: String(row.name ?? ""),
|
|
271
|
+
passed: row.passed === true || row.conclusion === "success"
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
217
274
|
function parseResultArtifact(value) {
|
|
218
275
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
219
276
|
return { ok: false, refusal: "malformedJson" };
|
|
@@ -223,22 +280,55 @@ function parseResultArtifact(value) {
|
|
|
223
280
|
if (unknown.length > 0) {
|
|
224
281
|
return { ok: false, refusal: "unknownKeys" };
|
|
225
282
|
}
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
283
|
+
const versioned = "schema" in bag || "version" in bag;
|
|
284
|
+
if (versioned && (bag.schema !== RESULT_SCHEMA_NAME || bag.version !== RESULT_SCHEMA_VERSION)) {
|
|
285
|
+
return { ok: false, refusal: "unsupportedSchema" };
|
|
286
|
+
}
|
|
287
|
+
let verification = null;
|
|
288
|
+
if (bag.verification !== void 0) {
|
|
289
|
+
if (typeof bag.verification !== "object" || bag.verification === null || Array.isArray(bag.verification)) {
|
|
290
|
+
return { ok: false, refusal: "malformedJson" };
|
|
291
|
+
}
|
|
292
|
+
verification = bag.verification;
|
|
293
|
+
if (Object.keys(verification).some((key) => !VERIFICATION_KEYS.has(key))) {
|
|
294
|
+
return { ok: false, refusal: "unknownKeys" };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const digest = normaliseResultDigest(bag.digest);
|
|
298
|
+
if (digest.length > 0 || !versioned) {
|
|
299
|
+
if (!RESULT_DIGEST_PATTERN.test(digest)) {
|
|
300
|
+
return { ok: false, refusal: "badDigest" };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const reusedDigest = normaliseResultDigest(bag.reusedDigest);
|
|
304
|
+
if (reusedDigest.length > 0 && !RESULT_DIGEST_PATTERN.test(reusedDigest)) {
|
|
228
305
|
return { ok: false, refusal: "badDigest" };
|
|
229
306
|
}
|
|
230
|
-
const result = {
|
|
307
|
+
const result = {};
|
|
308
|
+
if (digest.length > 0) result.digest = digest;
|
|
309
|
+
if (versioned) result.schemaVersion = RESULT_SCHEMA_VERSION;
|
|
231
310
|
if (typeof bag.imageRepository === "string" && bag.imageRepository.trim().length > 0) {
|
|
232
311
|
result.imageRepository = bag.imageRepository.trim();
|
|
233
312
|
}
|
|
234
313
|
if (Array.isArray(bag.tags)) {
|
|
235
|
-
result.tags = bag.tags.filter((tag) => typeof tag === "string");
|
|
314
|
+
result.tags = bag.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
|
236
315
|
}
|
|
237
316
|
if (bag.secretCheck === "passed" || bag.secretCheck === "failed" || bag.secretCheck === "not_needed") {
|
|
238
317
|
result.secretCheck = bag.secretCheck;
|
|
239
318
|
}
|
|
240
|
-
if (
|
|
241
|
-
result.
|
|
319
|
+
if (typeof bag.buildId === "string" && bag.buildId.trim().length > 0) {
|
|
320
|
+
result.buildId = bag.buildId.trim();
|
|
321
|
+
}
|
|
322
|
+
if (typeof bag.sha === "string" && COMMIT_SHA_PATTERN.test(bag.sha.trim())) {
|
|
323
|
+
result.sha = bag.sha.trim().toLowerCase();
|
|
324
|
+
}
|
|
325
|
+
if (reusedDigest.length > 0) result.reusedDigest = reusedDigest;
|
|
326
|
+
if (typeof bag.localBuild === "boolean") result.localBuild = bag.localBuild;
|
|
327
|
+
if (Array.isArray(bag.smoke) || Array.isArray(verification?.smoke)) {
|
|
328
|
+
result.smoke = [...passRows(bag.smoke), ...passRows(verification?.smoke)];
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(verification?.jobs)) {
|
|
331
|
+
result.verificationJobs = passRows(verification?.jobs);
|
|
242
332
|
}
|
|
243
333
|
return { ok: true, result };
|
|
244
334
|
}
|
|
@@ -280,7 +370,11 @@ async function readResultArtifact(port, input) {
|
|
|
280
370
|
} catch {
|
|
281
371
|
return { ok: false, refusal: "malformedJson" };
|
|
282
372
|
}
|
|
283
|
-
|
|
373
|
+
const parsed = parseResultArtifact(decoded);
|
|
374
|
+
if (parsed.ok && input.expectBuildId && parsed.result.buildId && parsed.result.buildId !== input.expectBuildId) {
|
|
375
|
+
return { ok: false, refusal: "buildMismatch" };
|
|
376
|
+
}
|
|
377
|
+
return parsed;
|
|
284
378
|
}
|
|
285
379
|
|
|
286
380
|
// src/runs/failure-classifier.ts
|
|
@@ -1286,7 +1380,7 @@ function resultStep(job, secretCheckPresent) {
|
|
|
1286
1380
|
`jq -n --arg buildId "\${{ inputs.ew_build_id }}" --arg sha "$EW_SHA" \\`,
|
|
1287
1381
|
` --arg imageRepository "$EW_IMAGE" --arg digest "$digest" --arg secretCheck "$secret_check" \\`,
|
|
1288
1382
|
` --arg tag "$EW_IMAGE:sha-$EW_SHA" \\`,
|
|
1289
|
-
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository
|
|
1383
|
+
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository,`,
|
|
1290
1384
|
` digest:$digest,tags:[$tag],secretCheck:$secretCheck,verification:{jobs:[],smoke:[]}}' > ${APP_BUILD_RESULT_ARTIFACT_FILE}`
|
|
1291
1385
|
] : [
|
|
1292
1386
|
"set -euo pipefail",
|
|
@@ -1296,7 +1390,7 @@ function resultStep(job, secretCheckPresent) {
|
|
|
1296
1390
|
`jq -n --arg buildId "\${{ inputs.ew_build_id }}" --arg sha "$EW_SHA" \\`,
|
|
1297
1391
|
` --arg imageRepository "$EW_IMAGE" --arg reusedDigest "$reused_digest" \\`,
|
|
1298
1392
|
` --argjson localBuild "$local_build" \\`,
|
|
1299
|
-
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository
|
|
1393
|
+
` '{schema:"ever-works-build-result",version:1,buildId:$buildId,sha:$sha,imageRepository:$imageRepository,`,
|
|
1300
1394
|
` reusedDigest:$reusedDigest,localBuild:$localBuild,verification:{jobs:[],smoke:[]}}' > ${APP_BUILD_RESULT_ARTIFACT_FILE}`
|
|
1301
1395
|
];
|
|
1302
1396
|
return [" - name: Write result", " if: always()", " run: |", ...scalarBlock(script, 10)];
|
|
@@ -1370,7 +1464,8 @@ async function readBranchHead(writer, branch) {
|
|
|
1370
1464
|
function repositoryWriteErrorCode(error) {
|
|
1371
1465
|
const candidate = error;
|
|
1372
1466
|
for (const value of [candidate?.code, candidate?.reason]) {
|
|
1373
|
-
if (value === "nonFastForward" || value === "refRejectedByRule" || value === "pullRequestExists"
|
|
1467
|
+
if (value === "nonFastForward" || value === "refRejectedByRule" || value === "pullRequestExists" || value === "workflowPermissionMissing")
|
|
1468
|
+
return value;
|
|
1374
1469
|
}
|
|
1375
1470
|
return null;
|
|
1376
1471
|
}
|
|
@@ -1412,14 +1507,36 @@ async function writeWorkflow(input, writer, protection) {
|
|
|
1412
1507
|
const removal = input.content === "";
|
|
1413
1508
|
if (removal) {
|
|
1414
1509
|
if (!existsOnTrackedBranch) return { state: "unchanged", storedWorkflowSha256: null };
|
|
1415
|
-
return
|
|
1510
|
+
return withWorkflowPermission(
|
|
1511
|
+
() => pullRequestDelivery(input, writer, path, contentSha256, handEdited, REMOVAL_COPY)
|
|
1512
|
+
);
|
|
1416
1513
|
}
|
|
1417
1514
|
const verdict = createdByAppWork && !handEdited && protection ? await isBranchProtected(protection, { owner, repo, branch: trackedBranch }) : { protected: false};
|
|
1418
1515
|
const pullRequestPath = !createdByAppWork || handEdited || verdict.protected;
|
|
1419
1516
|
if (pullRequestPath) {
|
|
1420
|
-
return pullRequestDelivery(input, writer, path, contentSha256, handEdited);
|
|
1517
|
+
return withWorkflowPermission(() => pullRequestDelivery(input, writer, path, contentSha256, handEdited));
|
|
1518
|
+
}
|
|
1519
|
+
return withWorkflowPermission(() => directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch));
|
|
1520
|
+
}
|
|
1521
|
+
async function withWorkflowPermission(deliver) {
|
|
1522
|
+
try {
|
|
1523
|
+
return await deliver();
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
if (repositoryWriteErrorCode(error) !== "workflowPermissionMissing") throw error;
|
|
1526
|
+
const facts = error;
|
|
1527
|
+
return {
|
|
1528
|
+
state: "blocked",
|
|
1529
|
+
storedWorkflowSha256: null,
|
|
1530
|
+
blocked: {
|
|
1531
|
+
reason: "workflowPermissionMissing",
|
|
1532
|
+
detail: {
|
|
1533
|
+
cause: "workflowPermissionMissing",
|
|
1534
|
+
credential: typeof facts.credential === "string" ? facts.credential : "unknown",
|
|
1535
|
+
missing: typeof facts.missing === "string" ? facts.missing : "workflow"
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1421
1539
|
}
|
|
1422
|
-
return directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch);
|
|
1423
1540
|
}
|
|
1424
1541
|
async function directDelivery(input, writer, path, contentSha256, existsOnTrackedBranch) {
|
|
1425
1542
|
const { trackedBranch } = input.repository;
|
|
@@ -1664,6 +1781,234 @@ function createBuildValueSecretSync(dependencies) {
|
|
|
1664
1781
|
};
|
|
1665
1782
|
}
|
|
1666
1783
|
|
|
1784
|
+
// src/repo/octokit-repository-writer.ts
|
|
1785
|
+
var WORKFLOWS_DIRECTORY = ".github/workflows/";
|
|
1786
|
+
var RepositoryWriteError = class extends Error {
|
|
1787
|
+
constructor(code, message, cause, credential, missing) {
|
|
1788
|
+
super(message);
|
|
1789
|
+
this.code = code;
|
|
1790
|
+
this.cause = cause;
|
|
1791
|
+
this.credential = credential;
|
|
1792
|
+
this.missing = missing;
|
|
1793
|
+
this.name = "RepositoryWriteError";
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
function workflowPermissionMissingError(credential, cause) {
|
|
1797
|
+
const app = credential === "githubApp";
|
|
1798
|
+
return new RepositoryWriteError(
|
|
1799
|
+
"workflowPermissionMissing",
|
|
1800
|
+
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.',
|
|
1801
|
+
cause,
|
|
1802
|
+
credential,
|
|
1803
|
+
app ? "workflows:write" : "workflow"
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
var FULL_SHA2 = /^[0-9a-f]{40}$/i;
|
|
1807
|
+
function statusOf(error) {
|
|
1808
|
+
const status = error?.status;
|
|
1809
|
+
return typeof status === "number" ? status : void 0;
|
|
1810
|
+
}
|
|
1811
|
+
function messageOf(error) {
|
|
1812
|
+
const candidate = error;
|
|
1813
|
+
const parts = [];
|
|
1814
|
+
if (typeof candidate?.message === "string") parts.push(candidate.message);
|
|
1815
|
+
const data = candidate?.response?.data;
|
|
1816
|
+
if (typeof data?.message === "string") parts.push(data.message);
|
|
1817
|
+
if (Array.isArray(data?.errors)) {
|
|
1818
|
+
for (const entry of data.errors) {
|
|
1819
|
+
const message = entry?.message;
|
|
1820
|
+
if (typeof message === "string") parts.push(message);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return parts.join(" | ");
|
|
1824
|
+
}
|
|
1825
|
+
var RULE_REFUSAL = /protected branch|ruleset|rule violation|required status|required review|push declined|cannot be updated/i;
|
|
1826
|
+
var WORKFLOW_REFUSAL = /workflow[^|]*\b(scope|permission)|\b(scope|permission)[^|]*workflow/i;
|
|
1827
|
+
var INTEGRATION_REFUSAL = /resource not accessible by integration/i;
|
|
1828
|
+
function scopesFromHeader(headers) {
|
|
1829
|
+
const header = headers?.["x-oauth-scopes"];
|
|
1830
|
+
if (typeof header !== "string") return null;
|
|
1831
|
+
return header.split(",").map((scope) => scope.trim()).filter(Boolean);
|
|
1832
|
+
}
|
|
1833
|
+
function octokitRepositoryWriter(octokit, repository, credential = {}) {
|
|
1834
|
+
const base = { owner: repository.owner, repo: repository.repo };
|
|
1835
|
+
const kind = credential.credential ?? "unknown";
|
|
1836
|
+
async function headOf(ref) {
|
|
1837
|
+
if (FULL_SHA2.test(ref)) return ref;
|
|
1838
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/git/ref/{ref}", {
|
|
1839
|
+
...base,
|
|
1840
|
+
ref: `heads/${ref}`
|
|
1841
|
+
});
|
|
1842
|
+
const sha = data?.object?.sha;
|
|
1843
|
+
if (typeof sha !== "string") throw new Error(`github-actions-build: no head commit for ${ref}`);
|
|
1844
|
+
return sha;
|
|
1845
|
+
}
|
|
1846
|
+
async function assertCanWriteWorkflows() {
|
|
1847
|
+
if (kind === "githubApp") return;
|
|
1848
|
+
let scopes = credential.scopes ?? null;
|
|
1849
|
+
if (!scopes) {
|
|
1850
|
+
try {
|
|
1851
|
+
scopes = scopesFromHeader((await octokit.request("GET /user")).headers);
|
|
1852
|
+
} catch {
|
|
1853
|
+
scopes = null;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (scopes && !scopes.includes("workflow")) {
|
|
1857
|
+
throw workflowPermissionMissingError(kind === "pat" ? "pat" : "oauth");
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
function translateWorkflowRefusal(error, touchesWorkflows) {
|
|
1861
|
+
if (!touchesWorkflows || error instanceof RepositoryWriteError) return error;
|
|
1862
|
+
const message = messageOf(error);
|
|
1863
|
+
if (WORKFLOW_REFUSAL.test(message)) {
|
|
1864
|
+
return workflowPermissionMissingError(/github app|integration/i.test(message) ? "githubApp" : kind, error);
|
|
1865
|
+
}
|
|
1866
|
+
return error;
|
|
1867
|
+
}
|
|
1868
|
+
async function commit(input, touchesWorkflows) {
|
|
1869
|
+
const { data: baseCommit } = await octokit.request("GET /repos/{owner}/{repo}/git/commits/{commit_sha}", {
|
|
1870
|
+
...base,
|
|
1871
|
+
commit_sha: input.baseSha
|
|
1872
|
+
});
|
|
1873
|
+
const baseTree = baseCommit?.tree?.sha;
|
|
1874
|
+
if (typeof baseTree !== "string") {
|
|
1875
|
+
throw new Error(`github-actions-build: commit ${input.baseSha} has no tree`);
|
|
1876
|
+
}
|
|
1877
|
+
const tree = [];
|
|
1878
|
+
for (const file of input.files) {
|
|
1879
|
+
const { data: blob } = await octokit.request("POST /repos/{owner}/{repo}/git/blobs", {
|
|
1880
|
+
...base,
|
|
1881
|
+
content: file.content,
|
|
1882
|
+
encoding: file.encoding === "base64" ? "base64" : "utf-8"
|
|
1883
|
+
});
|
|
1884
|
+
const sha = blob?.sha;
|
|
1885
|
+
if (typeof sha !== "string") throw new Error(`github-actions-build: no blob sha for ${file.path}`);
|
|
1886
|
+
tree.push({ path: file.path, mode: "100644", type: "blob", sha });
|
|
1887
|
+
}
|
|
1888
|
+
const { data: newTree } = await octokit.request("POST /repos/{owner}/{repo}/git/trees", {
|
|
1889
|
+
...base,
|
|
1890
|
+
base_tree: baseTree,
|
|
1891
|
+
tree
|
|
1892
|
+
});
|
|
1893
|
+
const { data: created } = await octokit.request("POST /repos/{owner}/{repo}/git/commits", {
|
|
1894
|
+
...base,
|
|
1895
|
+
message: input.message,
|
|
1896
|
+
tree: newTree.sha,
|
|
1897
|
+
parents: [input.baseSha]
|
|
1898
|
+
});
|
|
1899
|
+
const commitSha = created.sha;
|
|
1900
|
+
try {
|
|
1901
|
+
await octokit.request("PATCH /repos/{owner}/{repo}/git/refs/{ref}", {
|
|
1902
|
+
...base,
|
|
1903
|
+
ref: `heads/${input.branch}`,
|
|
1904
|
+
sha: commitSha,
|
|
1905
|
+
// Never a force-push: a moved branch is `nonFastForward`.
|
|
1906
|
+
force: false
|
|
1907
|
+
});
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
const status = statusOf(error);
|
|
1910
|
+
const message = messageOf(error);
|
|
1911
|
+
if (status === 422 && /fast[ -]?forward/i.test(message)) {
|
|
1912
|
+
throw new RepositoryWriteError(
|
|
1913
|
+
"nonFastForward",
|
|
1914
|
+
`${input.branch} moved; retry from its new head`,
|
|
1915
|
+
error
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
if ((status === 409 || status === 422 || status === 403) && RULE_REFUSAL.test(message)) {
|
|
1919
|
+
throw new RepositoryWriteError("refRejectedByRule", `${input.branch} refused the update`, error);
|
|
1920
|
+
}
|
|
1921
|
+
if (touchesWorkflows && status === 403 && INTEGRATION_REFUSAL.test(message)) {
|
|
1922
|
+
throw workflowPermissionMissingError("githubApp", error);
|
|
1923
|
+
}
|
|
1924
|
+
throw error;
|
|
1925
|
+
}
|
|
1926
|
+
return { commitSha };
|
|
1927
|
+
}
|
|
1928
|
+
return {
|
|
1929
|
+
async getFileContent(path, ref) {
|
|
1930
|
+
try {
|
|
1931
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/contents/{path}", {
|
|
1932
|
+
...base,
|
|
1933
|
+
path,
|
|
1934
|
+
...ref ?? repository.trackedBranch ? { ref: ref ?? repository.trackedBranch } : {}
|
|
1935
|
+
});
|
|
1936
|
+
const file = data;
|
|
1937
|
+
if (Array.isArray(data) || file?.type !== "file" || typeof file.content !== "string") return null;
|
|
1938
|
+
return {
|
|
1939
|
+
content: file.content,
|
|
1940
|
+
encoding: typeof file.encoding === "string" ? file.encoding : "base64"
|
|
1941
|
+
};
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
if (statusOf(error) === 404) return null;
|
|
1944
|
+
throw error;
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
async commitFiles(input) {
|
|
1948
|
+
const touchesWorkflows = input.files.some((file) => file.path.startsWith(WORKFLOWS_DIRECTORY));
|
|
1949
|
+
if (touchesWorkflows) await assertCanWriteWorkflows();
|
|
1950
|
+
try {
|
|
1951
|
+
return await commit(input, touchesWorkflows);
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
throw translateWorkflowRefusal(error, touchesWorkflows);
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
async createBranch(name, fromRef) {
|
|
1957
|
+
try {
|
|
1958
|
+
const { data } = await octokit.request("GET /repos/{owner}/{repo}/branches/{branch}", {
|
|
1959
|
+
...base,
|
|
1960
|
+
branch: name
|
|
1961
|
+
});
|
|
1962
|
+
const branch = data;
|
|
1963
|
+
return {
|
|
1964
|
+
name: branch.name ?? name,
|
|
1965
|
+
commit: branch.commit?.sha ?? "",
|
|
1966
|
+
isDefault: false,
|
|
1967
|
+
isProtected: branch.protected === true
|
|
1968
|
+
};
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
if (statusOf(error) !== 404) throw error;
|
|
1971
|
+
}
|
|
1972
|
+
const sha = await headOf(fromRef);
|
|
1973
|
+
await octokit.request("POST /repos/{owner}/{repo}/git/refs", {
|
|
1974
|
+
...base,
|
|
1975
|
+
ref: `refs/heads/${name}`,
|
|
1976
|
+
sha
|
|
1977
|
+
});
|
|
1978
|
+
return { name, commit: sha, isDefault: false };
|
|
1979
|
+
},
|
|
1980
|
+
async createPullRequest(options) {
|
|
1981
|
+
try {
|
|
1982
|
+
const { data } = await octokit.request("POST /repos/{owner}/{repo}/pulls", {
|
|
1983
|
+
...base,
|
|
1984
|
+
title: options.title,
|
|
1985
|
+
head: options.headOwner ? `${options.headOwner}:${options.head}` : options.head,
|
|
1986
|
+
base: options.base,
|
|
1987
|
+
...options.body ? { body: options.body } : {},
|
|
1988
|
+
...options.draft ? { draft: true } : {}
|
|
1989
|
+
});
|
|
1990
|
+
const pr = data;
|
|
1991
|
+
return {
|
|
1992
|
+
number: pr.number,
|
|
1993
|
+
title: pr.title ?? options.title,
|
|
1994
|
+
state: pr.state === "closed" ? "closed" : "open",
|
|
1995
|
+
head: options.head,
|
|
1996
|
+
base: options.base,
|
|
1997
|
+
url: pr.html_url ?? "",
|
|
1998
|
+
createdAt: pr.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1999
|
+
updatedAt: pr.updated_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2000
|
+
...pr.body ? { body: pr.body } : {}
|
|
2001
|
+
};
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
if (statusOf(error) === 422 && /pull request already exists/i.test(messageOf(error))) {
|
|
2004
|
+
throw new RepositoryWriteError("pullRequestExists", "an open pull request already exists", error);
|
|
2005
|
+
}
|
|
2006
|
+
throw error;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1667
2012
|
// src/settings.schema.ts
|
|
1668
2013
|
var LARGER_RUNNER_LABEL_PATTERN = "^[A-Za-z0-9._-]{1,64}$";
|
|
1669
2014
|
var LARGER_RUNNER_MEMORY_MIN_GIB = 8;
|
|
@@ -1955,7 +2300,9 @@ var GitHubActionsBuildPlugin = class {
|
|
|
1955
2300
|
buildInputsHash: computeBuildInputsHash(input.values ?? []),
|
|
1956
2301
|
blocked: {
|
|
1957
2302
|
reason: written.blocked?.reason ?? "workflowWriteFailed",
|
|
1958
|
-
|
|
2303
|
+
// The whole detail: `workflowPermissionMissing` carries which credential
|
|
2304
|
+
// and which permission, which is what the member's fix depends on.
|
|
2305
|
+
detail: { ...written.blocked?.detail ?? { cause: "unknown" } }
|
|
1959
2306
|
}
|
|
1960
2307
|
};
|
|
1961
2308
|
}
|
|
@@ -2083,13 +2430,16 @@ var GitHubActionsBuildPlugin = class {
|
|
|
2083
2430
|
const jobs = await port.listRunJobs({ repository, runId });
|
|
2084
2431
|
const snapshot = observeRun({ run, jobs, mode: "build" });
|
|
2085
2432
|
const finished = snapshot.status !== "queued" && snapshot.status !== "running";
|
|
2086
|
-
const artifact = finished ? await readResultArtifact(port, { repository, runId }) : null;
|
|
2433
|
+
const artifact = finished ? await readResultArtifact(port, { repository, runId, expectBuildId: ref.buildId }) : null;
|
|
2087
2434
|
const result = artifact?.ok ? artifact.result : null;
|
|
2088
2435
|
const failure = snapshot.status === "failed" ? await this.classifyRunFailure({ port, repository, run, jobs, snapshot, result, redact }) : null;
|
|
2089
2436
|
const pushLogDigest = result && snapshot.status === "succeeded" && isPushingTrigger(snapshot.trigger) ? await this.readPushLogDigest({ port, repository, jobs, sha: snapshot.commitSha }) : void 0;
|
|
2090
2437
|
return {
|
|
2091
2438
|
...snapshot,
|
|
2092
|
-
|
|
2439
|
+
// A versioned result with no digest (a failed build, a pull request, a
|
|
2440
|
+
// verify job) still reports its secret check — it just claims no image.
|
|
2441
|
+
...result?.secretCheck && !result.digest ? { secretCheck: result.secretCheck } : {},
|
|
2442
|
+
...result?.digest ? {
|
|
2093
2443
|
// `confirmed: false` without exception. This plugin reads the digest
|
|
2094
2444
|
// the member's own CI claimed; confirming it against the registry is
|
|
2095
2445
|
// `checkImageAccess` (T14), compared by `AppBuildsService.finalize`,
|
|
@@ -2244,6 +2594,78 @@ var GitHubActionsBuildPlugin = class {
|
|
|
2244
2594
|
registryFetch() {
|
|
2245
2595
|
return (url, init) => fetch(url, init);
|
|
2246
2596
|
}
|
|
2597
|
+
/**
|
|
2598
|
+
* APW-05 T21a / T12 remainder — run discovery (plan §7.4a, `APW05-G01`).
|
|
2599
|
+
*
|
|
2600
|
+
* The newest runs of the build workflow, so a push or pull-request Build is
|
|
2601
|
+
* recorded even when no `workflow_run` delivery reaches the platform (a
|
|
2602
|
+
* personal fork connected through OAuth has no App installation to deliver
|
|
2603
|
+
* from). `If-None-Match` with the stored `etag` makes an idle repository cost
|
|
2604
|
+
* a 304 — which GitHub does not count against the rate limit.
|
|
2605
|
+
*
|
|
2606
|
+
* The mapping is `githubRunToBuildRunRef` from contracts, the SAME function
|
|
2607
|
+
* the webhook consumer uses, so a polled run and a delivered run are the same
|
|
2608
|
+
* `BuildRunRef` and converge on one Build row.
|
|
2609
|
+
*/
|
|
2610
|
+
async listRecentRuns(input, auth) {
|
|
2611
|
+
const port = this.actionsPort(auth);
|
|
2612
|
+
const repository = { owner: input.repository.owner, repo: input.repository.name };
|
|
2613
|
+
const workflowFile = input.workflowPath.split("/").pop() || WORKFLOW_FILE_NAME;
|
|
2614
|
+
const perPage = Math.max(1, Math.min(APP_BUILD_RUN_DISCOVERY_PER_PAGE, Math.floor(input.perPage) || 1));
|
|
2615
|
+
const page = port.listWorkflowRunsConditional ? await port.listWorkflowRunsConditional({
|
|
2616
|
+
repository,
|
|
2617
|
+
workflowFile,
|
|
2618
|
+
perPage,
|
|
2619
|
+
...input.etag ? { etag: input.etag } : {}
|
|
2620
|
+
}) : { notModified: false, runs: await port.listWorkflowRuns({ repository, workflowFile, perPage }) };
|
|
2621
|
+
if (page.notModified) {
|
|
2622
|
+
return { notModified: true, ...page.etag ? { etag: page.etag } : {}, runs: [] };
|
|
2623
|
+
}
|
|
2624
|
+
const runs = page.runs.map((run) => githubRunToBuildRunRef(run)).filter((run) => run !== null);
|
|
2625
|
+
return { notModified: false, ...page.etag ? { etag: page.etag } : {}, runs };
|
|
2626
|
+
}
|
|
2627
|
+
/**
|
|
2628
|
+
* APW-05 T16 remainder — the clone-free writer `prepareRepository` delivers the
|
|
2629
|
+
* workflow through, over this plugin's own Octokit (see
|
|
2630
|
+
* `repo/octokit-repository-writer.ts` on why the plugin supplies it).
|
|
2631
|
+
*/
|
|
2632
|
+
repositoryWriter(repository, auth) {
|
|
2633
|
+
return octokitRepositoryWriter(this.octokitFor(auth), repository, {
|
|
2634
|
+
credential: auth.credential,
|
|
2635
|
+
scopes: auth.scopes
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Plan §5 — a Rebuild's `commitSha` must be reachable from the tracked branch
|
|
2640
|
+
* (2026-10-17). `GET …/compare/{sha}...{branch}`: `ahead` or `identical` means
|
|
2641
|
+
* the commit is in the branch's history; `behind` / `diverged` means it is not
|
|
2642
|
+
* (a commit only a fork's network carries is `diverged`), and 404/422 means
|
|
2643
|
+
* GitHub does not know it here. Any other failure is a throw.
|
|
2644
|
+
*/
|
|
2645
|
+
async isCommitReachable(input, auth) {
|
|
2646
|
+
try {
|
|
2647
|
+
const { data } = await this.octokitFor(auth).request("GET /repos/{owner}/{repo}/compare/{basehead}", {
|
|
2648
|
+
owner: input.repository.owner,
|
|
2649
|
+
repo: input.repository.repo,
|
|
2650
|
+
basehead: `${input.sha}...${input.branch}`,
|
|
2651
|
+
per_page: 1
|
|
2652
|
+
});
|
|
2653
|
+
const status = data?.status;
|
|
2654
|
+
return status === "ahead" || status === "identical";
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
const status = error?.status;
|
|
2657
|
+
if (status === 404 || status === 422) return false;
|
|
2658
|
+
throw error;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
/** §4.10 — the per-run prompted-value secret's removal (T12's `secret-sync.ts`, now on the contract). */
|
|
2662
|
+
async deleteVerifyPromptedSecret(repository, auth) {
|
|
2663
|
+
return createBuildValueSecretSync({ port: this.secretPort(auth, repository) }).deleteVerifyPromptedSecret();
|
|
2664
|
+
}
|
|
2665
|
+
/** The Octokit client for one call — a seam a spec overrides, like {@link actionsPort}. */
|
|
2666
|
+
octokitFor(auth) {
|
|
2667
|
+
return new Octokit({ auth: auth.token });
|
|
2668
|
+
}
|
|
2247
2669
|
/** APW-05 T12 — the run's own page, which is where a member reads its logs. */
|
|
2248
2670
|
async getLogsUrl(ref, auth) {
|
|
2249
2671
|
const port = this.actionsPort(auth);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ever-works/github-actions-build-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "GitHub Actions build plugin for Ever Works - turns a commit into an image through a workflow Ever Works writes into the repository (APW-05 plan §4.3)",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ever Co. LTD",
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
"test:coverage": "vitest run --coverage"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ever-works/contracts": "^1.0.
|
|
37
|
-
"fflate": "^0.
|
|
36
|
+
"@ever-works/contracts": "^1.0.7",
|
|
37
|
+
"fflate": "^0.8.3",
|
|
38
38
|
"libsodium-wrappers": "^0.7.15",
|
|
39
39
|
"octokit": "^4.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@ever-works/plugin": "^1.2.
|
|
42
|
+
"@ever-works/plugin": "^1.2.6"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@ever-works/plugin": "^1.2.
|
|
45
|
+
"@ever-works/plugin": "^1.2.6",
|
|
46
46
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
47
47
|
"@types/node": "^26.6.4",
|
|
48
48
|
"ajv": "^8.18.0",
|
|
49
|
-
"js-yaml": "^5.
|
|
49
|
+
"js-yaml": "^5.5.0",
|
|
50
50
|
"tsup": "^8.4.0",
|
|
51
51
|
"typescript": "^5.7.3",
|
|
52
|
-
"vitest": "^
|
|
52
|
+
"vitest": "^5.0.5"
|
|
53
53
|
},
|
|
54
54
|
"everworks": {
|
|
55
55
|
"plugin": {
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"url": "https://github.com/ever-works/ever-works/issues"
|
|
81
81
|
},
|
|
82
82
|
"everworksRelease": {
|
|
83
|
-
"contentHash": "sha256:
|
|
84
|
-
"commit": "
|
|
85
|
-
"source": "https://github.com/ever-works/ever-works/tree/
|
|
83
|
+
"contentHash": "sha256:059c27cd51df7113427cab5ec39e002c6500325d72f3f868073227280751a09e",
|
|
84
|
+
"commit": "48bb7f0a20744cbdfae87675743421804e100af6",
|
|
85
|
+
"source": "https://github.com/ever-works/ever-works/tree/48bb7f0a20744cbdfae87675743421804e100af6/packages/plugins/github-actions-build"
|
|
86
86
|
}
|
|
87
87
|
}
|