// epm/src/registry.el — Gitea-backed vessel registry // // Vessels are stored as Gitea repositories under a shared org: // // repo: / at https://git.neuralplatform.ai // versions: Gitea release tags (e.g. "0.1.0") // archive: release asset named "-.tar.gz" // // All registry read operations use unauthenticated HTTP GET. // registry_publish requires EPM_TOKEN to be set. // // Configuration (environment variables): // EPM_GITEA_API — Gitea API base URL (default: https://git.neuralplatform.ai/api/v1) // EPM_REGISTRY_ORG — org name that hosts vessel repos (default: neuron-technologies) // EPM_TOKEN — Gitea personal access token (required for publish) // ── Cross-module forward declarations ───────────────────────────────────────── // These symbols are defined in sibling epm modules or the El runtime and are // resolved at link time. The `extern fn` decls give elc the C prototype so the // generated registry.c compiles cleanly under strict compilers (gcc>=14 / clang) // that reject implicit function declarations. Signature arity must match the // definition; return/param types are informational (all lower to el_val_t). extern fn config(key: String) -> String // El runtime builtin extern fn read_installed() -> String // install.el // ── Config helpers ──────────────────────────────────────────────────────────── // registry_api_url returns the Gitea API base URL with no trailing slash. fn registry_api_url() -> String { let u: String = config("EPM_GITEA_API") if str_eq(u, "") { return "https://git.neuralplatform.ai/api/v1" } let n: Int = str_len(u) if str_ends_with(u, "/") { return str_slice(u, 0, n - 1) } return u } // registry_org returns the Gitea org that hosts vessel repos. fn registry_org() -> String { let o: String = config("EPM_REGISTRY_ORG") if str_eq(o, "") { return "neuron-technologies" } return o } // registry_token returns the Gitea auth token for publish operations. fn registry_token() -> String { return config("EPM_TOKEN") } // ── Release lookups ─────────────────────────────────────────────────────────── // registry_find_release fetches a specific release by tag from Gitea. // GET /api/v1/repos///releases/tags/ // Returns the raw release JSON object, or "" if not found / on error. fn registry_find_release(name: String, version: String) -> String { let url: String = registry_api_url() + "/repos/" + registry_org() + "/" + name + "/releases/tags/" + version let token: String = registry_token() let resp: String = "" if str_eq(token, "") { let resp = http_get(url) } else { let headers: String = "Authorization: token " + token let resp = http_get_with_headers(url, headers) } if str_eq(resp, "") { return "" } // Gitea returns 404 as an error JSON or empty — check for "id" field let release_id: String = json_get_string(resp, "id") if str_eq(release_id, "") { return "" } return resp } // registry_list_releases fetches all releases for a vessel from Gitea. // GET /api/v1/repos///releases // Returns a JSON array of release objects, or "[]" on error. fn registry_list_releases(name: String) -> String { let url: String = registry_api_url() + "/repos/" + registry_org() + "/" + name + "/releases?limit=50" let token: String = registry_token() let resp: String = "" if str_eq(token, "") { let resp = http_get(url) } else { let headers: String = "Authorization: token " + token let resp = http_get_with_headers(url, headers) } if str_eq(resp, "") { return "[]" } // Verify it is an array let count: Int = json_array_len(resp) if count < 0 { return "[]" } return resp } // registry_latest_version returns the tag_name of the most recent release. // Gitea returns releases sorted by creation date (newest first). // Returns "" if there are no releases or the repo is missing. fn registry_latest_version(name: String) -> String { let releases: String = registry_list_releases(name) let count: Int = json_array_len(releases) if count == 0 { return "" } let first: String = json_array_get(releases, 0) return json_get_string(first, "tag_name") } // registry_find_asset_url scans a release JSON object for the source tarball. // Looks for an asset named "-.tar.gz". // Falls back to zipball_url if no matching asset is found. // Returns "" if the release JSON is empty. fn registry_find_asset_url(release_json: String, name: String, version: String) -> String { if str_eq(release_json, "") { return "" } let target_asset: String = name + "-" + version + ".tar.gz" let assets_json: String = json_get_raw(release_json, "assets") let asset_count: Int = json_array_len(assets_json) let i: Int = 0 while i < asset_count { let asset: String = json_array_get(assets_json, i) let asset_name: String = json_get_string(asset, "name") if str_eq(asset_name, target_asset) { return json_get_string(asset, "browser_download_url") } let i = i + 1 } // Fall back to zipball_url from the release let zipball: String = json_get_string(release_json, "zipball_url") return zipball } // ── Public API ──────────────────────────────────────────────────────────────── // registry_find looks up a vessel in the Gitea registry. // When version is "" the latest release is used. // // Returns a metadata JSON blob with keys: // name, version, description, download_url // Returns "" if the vessel or version is not found. fn registry_find(name: String, version: String) -> String { let ver: String = version if str_eq(ver, "") { let ver = registry_latest_version(name) if str_eq(ver, "") { return "" } } let release_json: String = registry_find_release(name, ver) if str_eq(release_json, "") { return "" } let description: String = json_get_string(release_json, "body") let download_url: String = registry_find_asset_url(release_json, name, ver) let esc_name: String = json_escape_string(name) let esc_ver: String = json_escape_string(ver) let esc_desc: String = json_escape_string(description) let esc_url: String = json_escape_string(download_url) return "{\"name\":\"" + esc_name + "\",\"version\":\"" + esc_ver + "\",\"description\":\"" + esc_desc + "\",\"download_url\":\"" + esc_url + "\"}" } // registry_list returns a JSON array of installed vessel metadata blobs. // Listing all org repos requires auth in many Gitea setups, so this // falls back to reading the local installed.json. fn registry_list() -> String { let installed: String = read_installed() // installed is a JSON object: {"name":"version",...} // We return an empty array — callers should use installed_version() per name if str_eq(installed, "") { return "[]" } if str_eq(installed, "{}") { return "[]" } return "[]" } // ── Publish ─────────────────────────────────────────────────────────────────── // registry_publish creates a Gitea release and uploads the source tarball. // // Steps: // 1. Build a tarball of the current directory (excluding .epm/, .git/) // 2. POST /repos///releases to create the release // 3. POST /repos///releases//assets to upload the tarball // // Returns the release ID string on success, "" on failure. fn registry_publish(name: String, version: String, description: String, entry: String, deps_json: String) -> String { let token: String = registry_token() if str_eq(token, "") { println("epm: error: EPM_TOKEN is required for publishing") return "" } let tarball: String = "/tmp/epm-publish-" + name + "-" + version + ".tar.gz" let asset_name: String = name + "-" + version + ".tar.gz" // Build the source tarball from the current directory let tar_cmd: String = "tar -czf " + tarball + " --exclude='.epm' --exclude='.git' --exclude='*.tar.gz' ." let tar_ret: Int = exec_command(tar_cmd) if tar_ret != 0 { println("epm: error: failed to create source tarball (exit " + native_int_to_str(tar_ret) + ")") return "" } // Build release body — include metadata as JSON comment let esc_desc: String = json_escape_string(description) let esc_entry: String = json_escape_string(entry) let body_text: String = description + "\n\n" let esc_body: String = json_escape_string(body_text) let esc_tag: String = json_escape_string(version) let esc_name_field: String = json_escape_string(name + " " + version) let release_body: String = "{\"tag_name\":\"" + esc_tag + "\",\"name\":\"" + esc_name_field + "\",\"body\":\"" + esc_body + "\",\"draft\":false,\"prerelease\":false}" let api_base: String = registry_api_url() let org: String = registry_org() let releases_url: String = api_base + "/repos/" + org + "/" + name + "/releases" let auth_header: String = "Authorization: token " + token let resp: String = http_post_json_with_headers(releases_url, auth_header, release_body) if str_eq(resp, "") { println("epm: error: failed to create release on Gitea (no response)") return "" } let err_msg: String = json_get_string(resp, "message") if !str_eq(err_msg, "") { println("epm: error from Gitea: " + err_msg) return "" } let release_id: String = json_get_string(resp, "id") if str_eq(release_id, "") { println("epm: error: Gitea release response missing id") return "" } // Upload the tarball as a release asset via curl (multipart form) let upload_url: String = api_base + "/repos/" + org + "/" + name + "/releases/" + release_id + "/assets?name=" + asset_name let curl_cmd: String = "curl -s -X POST -H \"Authorization: token " + token + "\" -H \"Content-Type: application/octet-stream\" --data-binary @" + tarball + " \"" + upload_url + "\"" let upload_resp: String = exec(curl_cmd) // Clean up tarball let rm_ret: Int = exec_command("rm -f " + tarball) if str_eq(upload_resp, "") { println("epm: warning: asset upload returned no response (release " + release_id + " was created)") } return release_id }