CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
action-registry.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| abfa9ad | 1 | /** |
| 2 | * Built-in action registry for workflow engine v2 (Block C1 / Sprint 1 — Agent 8). | |
| 3 | * | |
| 4 | * Workflow YAML steps of the form `uses: gluecron/<name>@<version>` are | |
| 5 | * resolved here. The registry is in-memory and populated eagerly at module | |
| 6 | * load by calling `registerAll()` once. Re-registering is idempotent so | |
| 7 | * repeated imports (e.g. in tests) do not error. | |
| 8 | * | |
| 9 | * The runner (Agent 5) is the only expected caller of `resolveAction`. It | |
| 10 | * constructs an `ActionContext` from the running job's state and awaits | |
| 11 | * `handler.run(ctx)`. Handlers MUST NOT throw — every built-in wraps its | |
| 12 | * body in try/catch and returns `{ exitCode: 1, stderr }` on failure so the | |
| 13 | * runner's control flow stays predictable. | |
| 14 | */ | |
| 15 | ||
| 16 | export type ActionContext = { | |
| 17 | with: Record<string, unknown>; | |
| 18 | env: Record<string, string>; | |
| 19 | workspace: string; // absolute path to checked-out code | |
| 20 | runId: string; | |
| 21 | jobId: string; | |
| 22 | repoId: string; | |
| 23 | commitSha?: string | null; | |
| 24 | ref?: string | null; | |
| 25 | }; | |
| 26 | ||
| 27 | export type ActionResult = { | |
| 28 | exitCode: number; // 0 = success | |
| 29 | outputs?: Record<string, string>; | |
| 30 | stdout?: string; | |
| 31 | stderr?: string; | |
| 32 | }; | |
| 33 | ||
| 34 | export type ActionHandler = { | |
| 35 | name: string; // e.g. 'gluecron/gatetest' | |
| 36 | version: string; // e.g. 'v1' | |
| 37 | run: (ctx: ActionContext) => Promise<ActionResult>; | |
| 38 | }; | |
| 39 | ||
| 40 | // Keyed by `${name}@${version}`. A secondary "latest" pointer per name is | |
| 41 | // maintained so `uses: gluecron/foo` (no version) still resolves. | |
| 42 | const handlers = new Map<string, ActionHandler>(); | |
| 43 | const latestByName = new Map<string, string>(); // name -> version | |
| 44 | ||
| 45 | export function registerAction(handler: ActionHandler): void { | |
| 46 | if (!handler || !handler.name || !handler.version) return; | |
| 47 | const key = `${handler.name}@${handler.version}`; | |
| 48 | handlers.set(key, handler); | |
| 49 | // Simple latest-wins policy: last registration for a given name becomes | |
| 50 | // the default. Built-ins register in deterministic order (see registerAll). | |
| 51 | latestByName.set(handler.name, handler.version); | |
| 52 | } | |
| 53 | ||
| 54 | /** | |
| 55 | * Parse `uses` into (name, version). `version` defaults to `v1` when | |
| 56 | * omitted. Trailing whitespace tolerated; empty input yields nulls. | |
| 57 | */ | |
| 58 | function parseUses(uses: string): { name: string; version: string | null } | null { | |
| 59 | if (!uses || typeof uses !== "string") return null; | |
| 60 | const trimmed = uses.trim(); | |
| 61 | if (!trimmed) return null; | |
| 62 | const at = trimmed.lastIndexOf("@"); | |
| 63 | if (at === -1) { | |
| 64 | return { name: trimmed, version: null }; | |
| 65 | } | |
| 66 | const name = trimmed.slice(0, at).trim(); | |
| 67 | const version = trimmed.slice(at + 1).trim(); | |
| 68 | if (!name) return null; | |
| 69 | return { name, version: version || null }; | |
| 70 | } | |
| 71 | ||
| 72 | export function resolveAction(uses: string): ActionHandler | null { | |
| 73 | const parsed = parseUses(uses); | |
| 74 | if (!parsed) return null; | |
| 75 | ||
| 76 | // Explicit version first. | |
| 77 | if (parsed.version) { | |
| 78 | const key = `${parsed.name}@${parsed.version}`; | |
| 79 | return handlers.get(key) ?? null; | |
| 80 | } | |
| 81 | ||
| 82 | // No version: try the latest registered, fall back to v1. | |
| 83 | const latest = latestByName.get(parsed.name); | |
| 84 | if (latest) { | |
| 85 | const key = `${parsed.name}@${latest}`; | |
| 86 | const h = handlers.get(key); | |
| 87 | if (h) return h; | |
| 88 | } | |
| 89 | return handlers.get(`${parsed.name}@v1`) ?? null; | |
| 90 | } | |
| 91 | ||
| 92 | export function listActions(): { name: string; version: string }[] { | |
| 93 | return Array.from(handlers.values()).map((h) => ({ | |
| 94 | name: h.name, | |
| 95 | version: h.version, | |
| 96 | })); | |
| 97 | } | |
| 98 | ||
| 99 | // ------------------------------------------------------------------------- | |
| 100 | // Built-in registration | |
| 101 | // ------------------------------------------------------------------------- | |
| 102 | ||
| 103 | import { checkoutAction } from "./actions/checkout-action"; | |
| 104 | import { gatetestAction } from "./actions/gatetest-action"; | |
| 105 | import { cacheAction } from "./actions/cache-action"; | |
| 106 | import { uploadArtifactAction } from "./actions/upload-artifact-action"; | |
| 107 | import { downloadArtifactAction } from "./actions/download-artifact-action"; | |
| 108 | ||
| 109 | let registered = false; | |
| 110 | ||
| 111 | /** | |
| 112 | * Register every built-in action. Idempotent: safe to call multiple times. | |
| 113 | * Called once at module load below, but exported for tests that want to | |
| 114 | * reset state explicitly. | |
| 115 | */ | |
| 116 | export function registerAll(): void { | |
| 117 | if (registered) return; | |
| 118 | registerAction(checkoutAction); | |
| 119 | registerAction(gatetestAction); | |
| 120 | registerAction(cacheAction); | |
| 121 | registerAction(uploadArtifactAction); | |
| 122 | registerAction(downloadArtifactAction); | |
| 123 | registered = true; | |
| 124 | } | |
| 125 | ||
| 126 | registerAll(); |