| | @@ -16,10 +16,12 @@ |
| 16 | 16 | type Workflow, |
| 17 | 17 | } from "@diffinery/workflow-yaml"; |
| 18 | 18 | import { decryptSecret, encryptSecret } from "../lib/crypto.js"; |
| 19 | +import { Directory } from "../lib/directory.js"; |
| 19 | 20 | import { HttpError, newId, now, randomToken, sha256 } from "../lib/util.js"; |
| 20 | 21 | import type { Env } from "../env.js"; |
| 21 | 22 | import type { RepoDO } from "./RepoDO.js"; |
| 22 | 23 | import { atLeast, type Caller } from "./types.js"; |
| 24 | +import { ReusableError, expandReusable, localKey, parseCallRef, type Expanded, type Source } from "./reusable.js"; |
| 23 | 25 | |
| 24 | 26 | const WORKFLOW_DIRS = [".github/workflows", ".diffinery/workflows"]; |
| 25 | 27 | const SUPPORTED_LABEL = /^(ubuntu-.*|linux|diffinery(-.*)?|self-hosted|x64|amd64)$/i; |
| | @@ -516,9 +518,13 @@ |
| 516 | 518 | async plan(runId: string): Promise<{ levels: string[][]; error?: string; concurrencyBlocked: boolean }> { |
| 517 | 519 | const run = this.runRow(runId); |
| 518 | 520 | if (run.status === "completed") return { levels: [], error: "run already completed", concurrencyBlocked: false }; |
| 519 | | - const wf = (await this.loadWorkflows(run.sha)).find((w) => w.path === run.workflow_path); |
| 520 | | - if (!wf?.workflow) return { levels: [], error: `workflow file not found or invalid: ${wf?.errors.join("; ") ?? run.workflow_path}`, concurrencyBlocked: false }; |
| 521 | | - const { order, errors } = jobOrder(wf.workflow); |
| 521 | + let exp: Expanded; |
| 522 | + try { |
| 523 | + exp = await this.expanded(run); |
| 524 | + } catch (e) { |
| 525 | + return { levels: [], error: (e as Error).message, concurrencyBlocked: false }; |
| 526 | + } |
| 527 | + const { order, errors } = jobOrder(exp.workflow); |
| 522 | 528 | if (errors.length) return { levels: [], error: errors.map((e) => e.message).join("; "), concurrencyBlocked: false }; |
| 523 | 529 | const blocked = |
| 524 | 530 | !!run.concurrency_group && |
| | @@ -527,37 +533,140 @@ |
| 527 | 533 | return { levels: order, concurrencyBlocked: blocked }; |
| 528 | 534 | } |
| 529 | 535 | |
| 530 | | - private needsResults(runId: string, keys: string[]): Record<string, NeedsResult> { |
| 531 | | - const out: Record<string, NeedsResult> = {}; |
| 532 | | - for (const key of keys) { |
| 533 | | - const rows = this.sql.exec<JobRow>("SELECT * FROM jobs WHERE run_id = ? AND job_key = ?", runId, key).toArray(); |
| 534 | | - let result: Conclusion = "success"; |
| 535 | | - const outputs: Record<string, string> = {}; |
| 536 | | - if (rows.length === 0) result = "skipped"; |
| 537 | | - for (const r of rows) { |
| 538 | | - Object.assign(outputs, JSON.parse(r.outputs)); |
| 539 | | - const c = r.conclusion === "timed_out" ? "failure" : (r.conclusion ?? "failure"); |
| 540 | | - if (c === "failure") result = "failure"; |
| 541 | | - else if (c === "cancelled" && result !== "failure") result = "cancelled"; |
| 542 | | - else if (c === "skipped" && result === "success" && rows.length === 1) result = "skipped"; |
| 536 | + /** The run's workflow with reusable workflow calls expanded (see ./reusable.ts). */ |
| 537 | + private async expanded(run: RunRow): Promise<Expanded> { |
| 538 | + const wf = (await this.loadWorkflows(run.sha)).find((w) => w.path === run.workflow_path); |
| 539 | + if (!wf?.workflow) throw new ReusableError(`workflow file not found or invalid: ${wf?.errors.join("; ") || run.workflow_path}`); |
| 540 | + return expandReusable(wf.workflow, { repoId: null, sha: run.sha }, (uses, from) => this.loadCalled(uses, from)); |
| 541 | + } |
| 542 | + |
| 543 | + /** Load a called workflow: `./path` from the same commit, or `owner/repo/path@ref` from a repository on this instance. */ |
| 544 | + private async loadCalled(uses: string, from: Source): Promise<{ workflow: Workflow; source: Source }> { |
| 545 | + const ref = parseCallRef(uses); |
| 546 | + const m = this.repo.meta; |
| 547 | + let source: Source; |
| 548 | + let text: string | null; |
| 549 | + if (ref.kind === "local" && from.repoId === null) { |
| 550 | + source = from; |
| 551 | + const b = await this.repo.blob(from.sha, ref.path); |
| 552 | + text = b && !b.binary ? fromUtf8(b.content) : null; |
| 553 | + } else { |
| 554 | + let repoId: string; |
| 555 | + if (ref.kind === "local") repoId = from.repoId!; |
| 556 | + else if (ref.owner.toLowerCase() === m.owner.toLowerCase() && ref.repo.toLowerCase() === m.name.toLowerCase()) repoId = m.repoId; |
| 557 | + else { |
| 558 | + const target = await new Directory(this.env).repo(ref.owner, ref.repo); |
| 559 | + // Like GitHub: public and internal workflows can be shared; private ones only within the same owner. |
| 560 | + if (!target || (target.visibility === "private" && target.owner_id !== m.ownerId)) throw new Error(`repository ${ref.owner}/${ref.repo} not found, or its workflows are not shared with ${m.owner}/${m.name}`); |
| 561 | + repoId = target.id; |
| 543 | 562 | } |
| 544 | | - out[key] = { result, outputs }; |
| 563 | + if (repoId === m.repoId) { |
| 564 | + const sha = ref.kind === "local" ? from.sha : await this.repo.resolve(ref.ref); |
| 565 | + if (!sha) throw new Error(`unknown ref ${ref.kind === "remote" ? ref.ref : ""}`); |
| 566 | + source = { repoId: null, sha }; |
| 567 | + const b = await this.repo.blob(sha, ref.path); |
| 568 | + text = b && !b.binary ? fromUtf8(b.content) : null; |
| 569 | + } else { |
| 570 | + const stub = this.env.REPO.get(this.env.REPO.idFromName(repoId)); |
| 571 | + const sha = ref.kind === "local" ? from.sha : await stub.resolve(ref.ref); |
| 572 | + if (!sha) throw new Error(`unknown ref ${ref.kind === "remote" ? ref.ref : ""} in ${ref.kind === "remote" ? `${ref.owner}/${ref.repo}` : "the called repository"}`); |
| 573 | + source = { repoId, sha }; |
| 574 | + const b = await stub.blob(sha, ref.path); |
| 575 | + text = b && !b.binary ? fromUtf8(b.content) : null; |
| 576 | + } |
| 545 | 577 | } |
| 578 | + if (text === null) throw new Error(`${uses} not found`); |
| 579 | + const { workflow, errors } = parseWorkflow(text, ref.path); |
| 580 | + const fatal = errors.filter((e) => !isWarning(e)); |
| 581 | + if (!workflow || fatal.length) throw new Error(`${uses} is invalid: ${fatal.map((e) => `${e.path}: ${e.message}`).join("; ")}`); |
| 582 | + return { workflow, source }; |
| 583 | + } |
| 584 | + |
| 585 | + /** `needs` context for a job or call: keys are as written inside its own workflow. */ |
| 586 | + private async needsResults(run: RunRow, exp: Expanded, keys: string[], scope: string | null): Promise<Record<string, NeedsResult>> { |
| 587 | + const out: Record<string, NeedsResult> = {}; |
| 588 | + for (const key of keys) out[localKey(key, scope)] = await this.resultOf(run, exp, key); |
| 546 | 589 | return out; |
| 547 | 590 | } |
| 548 | 591 | |
| 592 | + /** Result and outputs of a job, or of a reusable workflow call (from its `on.workflow_call.outputs`). */ |
| 593 | + private async resultOf(run: RunRow, exp: Expanded, key: string): Promise<NeedsResult> { |
| 594 | + const call = exp.calls.get(key); |
| 595 | + const rows = call |
| 596 | + ? this.sql.exec<JobRow>("SELECT * FROM jobs WHERE run_id = ? AND substr(job_key, 1, ?) = ?", run.id, key.length + 1, `${key}/`).toArray() |
| 597 | + : this.sql.exec<JobRow>("SELECT * FROM jobs WHERE run_id = ? AND job_key = ?", run.id, key).toArray(); |
| 598 | + let result: Conclusion = rows.length === 0 ? "skipped" : "success"; |
| 599 | + let outputs: Record<string, string> = {}; |
| 600 | + for (const r of rows) { |
| 601 | + if (!call) Object.assign(outputs, JSON.parse(r.outputs)); |
| 602 | + const c = r.conclusion === "timed_out" ? "failure" : (r.conclusion ?? "failure"); |
| 603 | + if (c === "failure") result = "failure"; |
| 604 | + else if (c === "cancelled" && result !== "failure") result = "cancelled"; |
| 605 | + } |
| 606 | + if (rows.length && rows.every((r) => r.conclusion === "skipped")) result = "skipped"; |
| 607 | + if (call && Object.keys(call.workflow.on.workflow_call?.outputs ?? {}).length) { |
| 608 | + const jobs: Record<string, NeedsResult> = {}; |
| 609 | + for (const k of Object.keys(call.workflow.jobs)) jobs[k] = await this.resultOf(run, exp, `${key}/${k}`); |
| 610 | + const { inputs } = await this.callContext(run, exp, key, false); |
| 611 | + const ctx: ExprContext = { github: this.githubContext(run, localKey(key, call.parent), ""), jobs, inputs, vars: this.variables() }; |
| 612 | + outputs = {}; |
| 613 | + for (const [name, d] of Object.entries(call.workflow.on.workflow_call!.outputs!)) { |
| 614 | + try { |
| 615 | + const v = evaluateValue(d.value, ctx); |
| 616 | + outputs[name] = v === undefined || v === null ? "" : typeof v === "object" ? JSON.stringify(v) : String(v); |
| 617 | + } catch { |
| 618 | + outputs[name] = ""; |
| 619 | + } |
| 620 | + } |
| 621 | + } |
| 622 | + return { result, outputs }; |
| 623 | + } |
| 624 | + |
| 549 | 625 | /** |
| 626 | + * `inputs` and `secrets` inside a reusable workflow call (or at the top level when |
| 627 | + * `callKey` is null): inputs from the caller's `with` and the declared defaults, |
| 628 | + * secrets from `secrets: inherit` or the caller's mapping. |
| 629 | + */ |
| 630 | + private async callContext(run: RunRow, exp: Expanded, callKey: string | null, withSecrets: boolean): Promise<{ inputs: Record<string, unknown>; secrets: Record<string, string> }> { |
| 631 | + if (callKey === null) return { inputs: JSON.parse(run.inputs), secrets: withSecrets ? { ...(await this.orgSecrets()), ...(await this.repoSecrets()) } : {} }; |
| 632 | + const call = exp.calls.get(callKey)!; |
| 633 | + const parent = await this.callContext(run, exp, call.parent, withSecrets); |
| 634 | + const ctx: ExprContext = { github: this.githubContext(run, localKey(callKey, call.parent), ""), needs: await this.needsResults(run, exp, call.needs, call.parent), inputs: parent.inputs, vars: this.variables() }; |
| 635 | + const inputs: Record<string, unknown> = {}; |
| 636 | + for (const [k, d] of Object.entries(call.workflow.on.workflow_call?.inputs ?? {})) { |
| 637 | + let v = call.job.with && k in call.job.with ? evaluateValue(call.job.with[k], ctx) : evaluateValue(d.default, ctx); |
| 638 | + if (v === undefined || v === null || v === "") { |
| 639 | + if (d.required) throw new ReusableError(`jobs.${callKey}: input '${k}' is required by ${call.job.uses}`); |
| 640 | + v = d.type === "boolean" ? false : d.type === "number" ? 0 : ""; |
| 641 | + } |
| 642 | + inputs[k] = d.type === "boolean" ? v === true || v === "true" : d.type === "number" ? Number(v) : v; |
| 643 | + } |
| 644 | + const secrets: Record<string, string> = {}; |
| 645 | + if (withSecrets) { |
| 646 | + if (call.job.secrets === "inherit") Object.assign(secrets, parent.secrets); |
| 647 | + else for (const [name, expr] of Object.entries(call.job.secrets ?? {})) secrets[name.toUpperCase()] = String(evaluateValue(expr, { ...ctx, secrets: parent.secrets }) ?? ""); |
| 648 | + for (const [name, d] of Object.entries(call.workflow.on.workflow_call?.secrets ?? {})) { |
| 649 | + if (d.required && !secrets[name.toUpperCase()]) throw new ReusableError(`jobs.${callKey}: secret '${name}' is required by ${call.job.uses}`); |
| 650 | + } |
| 651 | + } |
| 652 | + return { inputs, secrets }; |
| 653 | + } |
| 654 | + |
| 655 | + /** |
| 550 | 656 | * Start every matrix instance of a job. Returns the ids the workflow must wait |
| 551 | 657 | * for (instances beyond max-parallel are queued and started as others finish). |
| 552 | 658 | */ |
| 553 | 659 | async startJobKey(runId: string, jobKey: string): Promise<{ jobIds: string[] }> { |
| 554 | 660 | const run = this.runRow(runId); |
| 555 | | - const wf = (await this.loadWorkflows(run.sha)).find((w) => w.path === run.workflow_path)!.workflow!; |
| 556 | | - const job = wf.jobs[jobKey]!; |
| 661 | + const exp = await this.expanded(run); |
| 662 | + const job = exp.workflow.jobs[jobKey]!; |
| 663 | + const owner = exp.workflowOf.get(jobKey)!; |
| 664 | + const parent = exp.parentOf.get(jobKey) ?? null; |
| 557 | 665 | const cancelled = run.conclusion === "cancelled"; |
| 558 | | - const needs = this.needsResults(runId, job.needs); |
| 559 | | - const status = statusFor(Object.values(needs), cancelled); |
| 560 | | - const baseCtx: ExprContext = { github: this.githubContext(run, jobKey, ""), needs, inputs: JSON.parse(run.inputs), vars: this.variables(), env: wf.env }; |
| 666 | + const chain: string[] = []; |
| 667 | + for (let c = parent; c; c = exp.calls.get(c)!.parent) chain.unshift(c); |
| 668 | + const prefix = chain.map((c) => exp.calls.get(c)!.job.name ?? localKey(c, exp.calls.get(c)!.parent)).join(" / "); |
| 669 | + const label = (name: string) => (prefix ? `${prefix} / ${name}` : name); |
| 561 | 670 | const fail = (name: string, message: string, conclusion: Conclusion = "failure") => { |
| 562 | 671 | const id = newId(); |
| 563 | 672 | const t = now(); |
| | @@ -565,35 +674,50 @@ |
| 565 | 674 | this.repo.broadcast(`run:${runId}`, { action: "job", jobId: id }); |
| 566 | 675 | return { jobIds: [] as string[] }; |
| 567 | 676 | }; |
| 677 | + const short = localKey(jobKey, parent); |
| 678 | + let needs: Record<string, NeedsResult>; |
| 679 | + let baseCtx: ExprContext; |
| 568 | 680 | let shouldRun: boolean; |
| 569 | 681 | try { |
| 570 | | - shouldRun = evaluateIf(job.if, baseCtx, status); |
| 682 | + // The callers decide first: a skipped call, or one whose needs failed, skips everything inside it. |
| 683 | + for (const c of chain) { |
| 684 | + const call = exp.calls.get(c)!; |
| 685 | + const callNeeds = await this.needsResults(run, exp, call.needs, call.parent); |
| 686 | + const outer = await this.callContext(run, exp, call.parent, false); |
| 687 | + const callCtx: ExprContext = { github: this.githubContext(run, localKey(c, call.parent), ""), needs: callNeeds, inputs: outer.inputs, vars: this.variables() }; |
| 688 | + if (!evaluateIf(call.job.if, callCtx, statusFor(Object.values(callNeeds), cancelled))) return fail(label(job.name ?? short), "", "skipped"); |
| 689 | + } |
| 690 | + // Resolve the call's inputs and secrets now, so a missing required one fails here rather than in the runner. |
| 691 | + if (parent) await this.callContext(run, exp, parent, true); |
| 692 | + needs = await this.needsResults(run, exp, exp.needs.get(jobKey) ?? [], parent); |
| 693 | + baseCtx = { github: this.githubContext(run, short, ""), needs, inputs: (await this.callContext(run, exp, parent, false)).inputs, vars: this.variables(), env: owner.env }; |
| 694 | + shouldRun = evaluateIf(job.if, baseCtx, statusFor(Object.values(needs), cancelled)); |
| 571 | 695 | } catch (e) { |
| 572 | | - return fail(job.name ?? jobKey, `Error evaluating 'if': ${(e as Error).message}`); |
| 696 | + return fail(label(job.name ?? short), e instanceof ReusableError ? e.message : `Error evaluating 'if': ${(e as Error).message}`); |
| 573 | 697 | } |
| 574 | | - if (!shouldRun) return fail(job.name ?? jobKey, "", "skipped"); |
| 575 | | - if (job.uses) return fail(job.name ?? jobKey, `Reusable workflows (jobs.${jobKey}.uses) are not supported yet on Diffinery.`); |
| 698 | + if (!shouldRun) return fail(label(job.name ?? short), "", "skipped"); |
| 576 | 699 | const labels = job.runsOn.map((l) => String(evaluateValue(l, baseCtx))); |
| 577 | 700 | const unsupported = labels.filter((l) => !SUPPORTED_LABEL.test(l)); |
| 578 | 701 | const selfHosted = labels.some((l) => l.toLowerCase() === "self-hosted"); |
| 579 | | - if (unsupported.length && !selfHosted) return fail(job.name ?? jobKey, `No Diffinery runner matches runs-on: ${unsupported.join(", ")}. Linux (ubuntu-*) runners are hosted; anything else needs runs-on: [self-hosted, ...] and a registered runner.`); |
| 580 | | - if (job.container) return fail(job.name ?? jobKey, "Job containers (jobs.<id>.container) are not supported yet; steps run directly on the Ubuntu runner image."); |
| 581 | | - if (job.services && Object.keys(job.services).length) return fail(job.name ?? jobKey, "Service containers (jobs.<id>.services) are not supported yet on Diffinery runners."); |
| 702 | + if (unsupported.length && !selfHosted) return fail(label(job.name ?? short), `No Diffinery runner matches runs-on: ${unsupported.join(", ")}. Linux (ubuntu-*) runners are hosted; anything else needs runs-on: [self-hosted, ...] and a registered runner.`); |
| 703 | + if (job.container) return fail(label(job.name ?? short), "Job containers (jobs.<id>.container) are not supported yet; steps run directly on the Ubuntu runner image."); |
| 704 | + if (job.services && Object.keys(job.services).length) return fail(label(job.name ?? short), "Service containers (jobs.<id>.services) are not supported yet on Diffinery runners."); |
| 582 | 705 | |
| 583 | 706 | let combos: Array<Record<string, unknown>>; |
| 584 | 707 | try { |
| 585 | 708 | combos = expandMatrix(job.strategy?.matrix, baseCtx); |
| 586 | 709 | } catch (e) { |
| 587 | | - return fail(job.name ?? jobKey, `Error expanding matrix: ${(e as Error).message}`); |
| 710 | + return fail(label(job.name ?? short), `Error expanding matrix: ${(e as Error).message}`); |
| 588 | 711 | } |
| 589 | | - if (combos.length > 256) return fail(job.name ?? jobKey, "A matrix may produce at most 256 jobs."); |
| 712 | + if (combos.length > 256) return fail(label(job.name ?? short), "A matrix may produce at most 256 jobs."); |
| 590 | 713 | const failFast = evaluateValue(job.strategy?.failFast ?? true, baseCtx) !== false; |
| 591 | 714 | const maxParallel = Number(evaluateValue(job.strategy?.maxParallel ?? combos.length, baseCtx)) || combos.length; |
| 592 | 715 | const ids: string[] = []; |
| 593 | 716 | combos.forEach((matrix, i) => { |
| 594 | 717 | const ctx = { ...baseCtx, matrix, strategy: { "fail-fast": failFast, "job-index": i, "job-total": combos.length, "max-parallel": maxParallel } }; |
| 595 | | - let name = job.name ? interpolate(job.name, ctx) : jobKey; |
| 596 | | - if (!job.name && Object.keys(matrix).length) name = `${jobKey} (${Object.values(matrix).map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join(", ")})`; |
| 718 | + let name = job.name ? interpolate(job.name, ctx) : short; |
| 719 | + if (!job.name && Object.keys(matrix).length) name = `${short} (${Object.values(matrix).map((v) => (typeof v === "object" ? JSON.stringify(v) : String(v))).join(", ")})`; |
| 720 | + name = label(name); |
| 597 | 721 | const timeout = Number(evaluateValue(job.timeoutMinutes, ctx)) || 360; |
| 598 | 722 | const id = newId(); |
| 599 | 723 | this.sql.exec( |
| | @@ -771,10 +895,11 @@ |
| 771 | 895 | const job = await this.authJob(jobId, token); |
| 772 | 896 | if (job.status !== "in_progress") throw new HttpError(409, "job is not running"); |
| 773 | 897 | const run = this.runRow(job.run_id); |
| 774 | | - const wf = (await this.loadWorkflows(run.sha)).find((w) => w.path === run.workflow_path)!.workflow!; |
| 775 | | - const def: Job = wf.jobs[job.job_key]!; |
| 898 | + const exp = await this.expanded(run); |
| 899 | + const def: Job = exp.workflow.jobs[job.job_key]!; |
| 900 | + const wf = exp.workflowOf.get(job.job_key)!; |
| 776 | 901 | const allowed = wf.permissions === "write-all" || (typeof wf.permissions === "object" && wf.permissions["id-token"] === "write") || (typeof def.permissions === "object" && def.permissions["id-token"] === "write") || def.permissions === "write-all"; |
| 777 | | - const ctx = this.githubContext(run, job.job_key, token) as Record<string, Json>; |
| 902 | + const ctx = this.githubContext(run, localKey(job.job_key, exp.parentOf.get(job.job_key) ?? null), token) as Record<string, Json>; |
| 778 | 903 | const github: Record<string, string> = {}; |
| 779 | 904 | for (const [k, v] of Object.entries(ctx)) if (typeof v === "string" || typeof v === "number") github[k] = String(v); |
| 780 | 905 | return { allowed, github }; |
| | @@ -783,31 +908,41 @@ |
| 783 | 908 | async runnerSpec(jobId: string, token: string): Promise<JobSpec> { |
| 784 | 909 | const job = await this.authJob(jobId, token); |
| 785 | 910 | const run = this.runRow(job.run_id); |
| 786 | | - const wf = (await this.loadWorkflows(run.sha)).find((w) => w.path === run.workflow_path)!.workflow!; |
| 787 | | - const def: Job = wf.jobs[job.job_key]!; |
| 911 | + const exp = await this.expanded(run); |
| 912 | + const def: Job = exp.workflow.jobs[job.job_key]!; |
| 913 | + const wf = exp.workflowOf.get(job.job_key)!; |
| 914 | + const parent = exp.parentOf.get(job.job_key) ?? null; |
| 915 | + const short = localKey(job.job_key, parent); |
| 916 | + let scope: { inputs: Record<string, unknown>; secrets: Record<string, string> }; |
| 917 | + try { |
| 918 | + scope = await this.callContext(run, exp, parent, true); |
| 919 | + } catch (e) { |
| 920 | + await this.completeJob(jobId, { conclusion: "failure", outputs: {}, failure: { message: (e as Error).message } }); |
| 921 | + throw new HttpError(409, (e as Error).message); |
| 922 | + } |
| 788 | 923 | const m = this.repo.meta; |
| 789 | 924 | const t = now(); |
| 790 | 925 | this.sql.exec("UPDATE jobs SET status = 'in_progress', started_at = ? WHERE id = ?", t, jobId); |
| 791 | 926 | this.repo.upsertCheck({ sha: run.sha, name: this.checkName(run, job), status: "in_progress", runId: run.id, jobId, detailsUrl: `${this.env.PUBLIC_URL}/${m.owner}/${m.name}/actions/runs/${run.id}/job/${jobId}` }); |
| 792 | 927 | this.repo.broadcast(`run:${run.id}`, { action: "job", jobId }); |
| 793 | | - const secrets: Record<string, string> = { ...(await this.orgSecrets()), ...(await this.repoSecrets()), GITHUB_TOKEN: token, DIFFINERY_TOKEN: token }; |
| 928 | + const secrets: Record<string, string> = { ...scope.secrets, GITHUB_TOKEN: token, DIFFINERY_TOKEN: token }; |
| 794 | 929 | const idToken = wf.permissions === "write-all" || (typeof wf.permissions === "object" && wf.permissions["id-token"] === "write") || (typeof def.permissions === "object" && def.permissions["id-token"] === "write") || def.permissions === "write-all"; |
| 795 | 930 | return { |
| 796 | 931 | runId: run.id, |
| 797 | 932 | jobId, |
| 798 | | - jobKey: job.job_key, |
| 933 | + jobKey: short, |
| 799 | 934 | name: job.name, |
| 800 | 935 | repo: { owner: m.owner, name: m.name, cloneUrl: `${this.env.PUBLIC_URL}/${m.owner}/${m.name}.git`, defaultBranch: m.defaultBranch }, |
| 801 | 936 | sha: run.sha, |
| 802 | 937 | ref: run.ref, |
| 803 | 938 | workflowPath: run.workflow_path, |
| 804 | | - github: this.githubContext(run, job.job_key, token), |
| 939 | + github: this.githubContext(run, short, token), |
| 805 | 940 | env: { ...wf.env, ...def.env }, |
| 806 | 941 | defaults: { run: { ...wf.defaults.run, ...def.defaults.run } }, |
| 807 | 942 | matrix: JSON.parse(job.matrix), |
| 808 | 943 | strategy: JSON.parse(job.strategy), |
| 809 | | - needs: this.needsResults(run.id, def.needs), |
| 810 | | - inputs: JSON.parse(run.inputs), |
| 944 | + needs: await this.needsResults(run, exp, exp.needs.get(job.job_key) ?? [], parent), |
| 945 | + inputs: scope.inputs, |
| 811 | 946 | vars: this.variables(), |
| 812 | 947 | secrets, |
| 813 | 948 | outputs: def.outputs, |