- every scratch file lives in `$TMPDIR/x_<pid>/` (tmp_dir/tmp_path in prelude.ludic), removed by main's new dispatch() → tmp_cleanup(); X_KEEP_TMP=1 keeps it. `x test` and `x check-*` may now run together. - `x test-lsp` (tools/x/lsp_test.ludic) replaces tools/test-lsp.py: the whole request stream is framed into one stdin file, the server runs to `exit`, and the response stream is parsed back by request id; adds a check that ludicc's own error is published on save - tools/x/forgejo.ludic replaces tools/ci/forgejo_release.py (curl with a 0600 header file; the token is no longer on the command line; LUDIC_FORGEJO_API for forks) - `x docs-palette --check` regenerates into scratch and compares, so the drift guard judges the working tree rather than git HEAD; the generator no longer emits a trailing blank line the formatter rejects - `x test-tools`: exit 2 from test-lsp / test-grammar.js is a visible skip, never a pass; the widget-prop test uses the `id: Root` syntax - tools/test-grammar.js: current vocabulary (property/model/handler/ prefab/scene/event/become/@Queries), LUDIC_NODE_MODULES, exit 2 on skip - tools/atlas.ludic rewritten in the current language (it did not compile) - operators.ludic / os.ludic registered in the suite - json.ludic: j_quote() writer helper Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
93 lines
4.2 KiB
Text
93 lines
4.2 KiB
Text
# forgejo.ludic — publish a release on the Forgejo instance: create the release
|
|
# for a tag (or reuse it), then attach every file from an assets directory.
|
|
# Replaces tools/ci/forgejo_release.py: the one piece of the release flow that
|
|
# speaks HTTP now does so through curl, driven from Ludic like every other
|
|
# external tool the runner uses.
|
|
#
|
|
# The API root comes from $LUDIC_FORGEJO_API (default: this project's repo);
|
|
# the token from $FORGEJO_TOKEN, passed to curl through a 0600 header file so
|
|
# it never appears on a command line.
|
|
#
|
|
# Idempotent-ish: an existing release for the tag is reused, and an asset whose
|
|
# name is already attached is skipped, not fatal.
|
|
|
|
const FORGEJO_API_DEFAULT: string = "https://git.workshopsoft.io/api/v1/repos/workshopsoft/ludic"
|
|
|
|
function forgejo_api() -> pointer { return getenv_or("LUDIC_FORGEJO_API", FORGEJO_API_DEFAULT) }
|
|
|
|
# the curl header file carrying the token (written once per process)
|
|
var forgejo_hdr: pointer = null
|
|
function forgejo_headers() -> pointer {
|
|
if forgejo_hdr == null {
|
|
forgejo_hdr = tmp_path("forgejo_headers.txt")
|
|
run(`umask 077 && printf 'Authorization: token %s\n' "$FORGEJO_TOKEN" > {forgejo_hdr}`)
|
|
}
|
|
return forgejo_hdr
|
|
}
|
|
|
|
# one API call: `extra` carries the method/body flags; returns the HTTP status
|
|
# and leaves the response body in `out`
|
|
function forgejo_call(path: pointer, extra: pointer, out: pointer) -> int {
|
|
let code = capture_line(`curl -sS -o {out} -w '%{{http_code}}' -H @{forgejo_headers()} {extra} '{forgejo_api()}{path}'`)
|
|
return str_to_int(code)
|
|
}
|
|
|
|
# create-or-reuse the release for `tag`, with `body_file` as its notes; returns
|
|
# the release JSON, or null after printing why
|
|
function forgejo_release(tag: pointer, body_file: pointer) -> JVal {
|
|
let out = tmp_path("forgejo_release.json")
|
|
let code = forgejo_call(`/releases/tags/{tag}`, "", out)
|
|
if code == 200 {
|
|
let rel = json_parse(read_file(out))
|
|
print(`release {tag} already exists (id {string(j_get(rel, "id").num)}); uploading assets`)
|
|
return rel
|
|
}
|
|
if code != 404 { err(`forgejo: GET /releases/tags/{tag} -> HTTP {string(code)}\n`); return null }
|
|
var body = ""
|
|
if shq(`test -f {body_file}`) { body = read_file(body_file) }
|
|
var prerelease = "false"
|
|
if Text.starts_with(tag, "v0.") { prerelease = "true" }
|
|
let req = tmp_path("forgejo_create.json")
|
|
write_file(req, `{{"tag_name":{j_quote(tag)},"name":{j_quote(tag)},"body":{j_quote(body)},"draft":false,"prerelease":{prerelease}}}`)
|
|
let ccode = forgejo_call("/releases", `-X POST -H 'Content-Type: application/json' --data-binary @{req}`, out)
|
|
if ccode != 201 { err(`forgejo: POST /releases -> HTTP {string(ccode)}\n`); return null }
|
|
let rel = json_parse(read_file(out))
|
|
print(`created release {tag} id {string(j_get(rel, "id").num)}`)
|
|
return rel
|
|
}
|
|
|
|
# attach every regular file in `dir` that the release does not already carry
|
|
function forgejo_upload_assets(rel: JVal, dir: pointer) -> bool {
|
|
if not shq(`test -d {dir}`) { return true }
|
|
let rid = j_get(rel, "id").num
|
|
let existing = new []pointer
|
|
let assets = j_get(rel, "assets")
|
|
var i = 0
|
|
while i < len(assets.kids) { push(existing, j_get(assets.kids[i], "name").s); i += 1 }
|
|
let names = Text.split(capture(`ls -1 {dir}`), "\n")
|
|
var ok_all = true
|
|
i = 0
|
|
while i < len(names) {
|
|
let name = Text.trim(names[i])
|
|
i += 1
|
|
if name == "" { continue }
|
|
if set_has(existing, name) { print(` skip (exists): {name}`); continue }
|
|
let path = `{dir}/{name}`
|
|
if not shq(`test -f {path}`) { continue }
|
|
let out = tmp_path("forgejo_asset.json")
|
|
let code = forgejo_call(`/releases/{string(rid)}/assets?name={name}`, `-X POST -F 'attachment=@{path}'`, out)
|
|
if code == 201 { print(` uploaded: {name} (HTTP {string(code)})`) }
|
|
else { err(` upload failed: {name} (HTTP {string(code)})\n`); ok_all = false }
|
|
}
|
|
return ok_all
|
|
}
|
|
|
|
# the whole flow; 0 on success
|
|
function forgejo_publish(tag: pointer, body_file: pointer, assets_dir: pointer) -> int {
|
|
if getenv_or("FORGEJO_TOKEN", "") == "" { err("forgejo: FORGEJO_TOKEN is not set\n"); return 1 }
|
|
let rel = forgejo_release(tag, body_file)
|
|
if rel == null { return 1 }
|
|
if not forgejo_upload_assets(rel, assets_dir) { return 1 }
|
|
print(`done: {forgejo_api()}/releases/tag/{tag}`)
|
|
return 0
|
|
}
|