Versioned, reusable GitHub Actions for publishing Logos modules.
A consumer repo (e.g. logos-co/logos-modules-v2 or any third-party fork)
holds the module submodules and calls these workflows. The mechanism itself
is version-pinned (@v1, @v2, ...) so consumers upgrade on their own
cadence.
For each module in your repo, the release.yml workflow:
- Reads
metadata.jsonto pick upnameandversion. - Builds the module's
.lgxon a per-variant matrix (Linux + macOS by default). - Merges the per-variant artifacts into a single multi-variant
.lgx(vialgx merge). lgx verify— fails the run if the package is invalid.- Optional signing (see Signing below).
lgx manifest --jsonto extract the embedded manifest, then build a sidecar JSON capturingreleasedAt,publisherRef,sha256,rootHash, the full manifest, and (when signed) the embeddedmanifest.sig.- Publishes a per-module GitHub release tagged
<module>-v<version>with the.lgxand the sidecar attached. - Dispatches
rebuild-index, which:- Walks every release in the repo.
- Reads each sidecar.
- Aggregates them into a single
index.json. - Uploads it to the rolling
indexrelease tag.
Clients (the lgpd CLI, the Logos package_downloader module) fetch
logos-repo.json from the repo root and index.json from the rolling
index release — that's the entire catalog contract.
# .github/workflows/release-chat.yml in your repo
on: { workflow_dispatch: {} }
jobs:
release:
uses: logos-co/logos-modules-release-action/.github/workflows/release.yml@v1
with:
module_path: submodules/logos-chat-module
signing_mode: inline # or "external" / "none"
secrets:
signing_key: ${{ secrets.LOGOS_SIGNING_KEY }}Repeat per module. Bumping the submodule pointer (and thereby its
metadata.json version) is what triggers a new release.
release.yml skips the (expensive) Nix build when a release tagged
<module>-v<version> already exists and carries both a .lgx and
a sidecar.json asset — i.e. that version is, or will be on the next
index rebuild, in the catalog. Re-running a workflow for an unchanged
submodule is therefore a fast no-op, and the umbrella "release all"
only rebuilds modules whose version actually moved.
- The release (not
index.json) is the source of truth — the index is derived from releases and eventually consistent, so release-existence is the correct, race-free gate. - A release missing either asset is treated as not published, so a half-finished prior run self-heals on the next trigger.
- On the skip path the action still nudges
rebuild-index, so "already published" promptly implies "in the index". - Force a rebuild/republish of the same version with
skip_if_published: false.
# .github/workflows/unpublish.yml in your repo
on: { workflow_dispatch: { inputs: {
module: { required: true, type: string },
version: { type: string, default: "" }, # blank = ALL versions
delete_tags: { type: boolean, default: true },
dry_run: { type: boolean, default: false } } } }
permissions: { contents: write }
jobs:
unpublish:
uses: logos-co/logos-modules-release-action/.github/workflows/unpublish.yml@v1
with:
module: ${{ inputs.module }}
version: ${{ inputs.version }}
delete_tags: ${{ inputs.delete_tags }}
dry_run: ${{ inputs.dry_run }}Deletes the matching GitHub release(s) (every <module>-v* when
version is blank, otherwise just <module>-v<version>), optionally
their git tags, then rebuilds index.json so clients stop offering the
removed package(s). The rolling index release is hard-guarded against
deletion. dry_run: true lists what would be removed and stops —
run that first; deletion is irreversible.
You also need a one-shot workflow that wires up rebuild-index for
automatic triggering, plus a top-level logos-repo.json at the repo
root:
# .github/workflows/rebuild-index.yml
on: { workflow_dispatch: {}, repository_dispatch: { types: [rebuild-index] } }
jobs:
rebuild:
uses: logos-co/logos-modules-release-action/.github/workflows/rebuild-index.yml@v1Three mutually exclusive signing_mode values:
| Mode | What runs | Where the key lives |
|---|---|---|
none |
nothing | n/a (unsigned release) |
inline |
lgx sign inside the workflow |
GitHub Actions signing_key secret (JWK string) |
external |
a user-supplied command (signing_command) |
Anywhere — Jenkins, HSM, hardware token, ... |
For signing_mode: external, the workflow runs signing_command with
two env vars set:
LGX_PATH— absolute path to the unsigned.lgxproduced by the build.LGX_SIGNED_OUT— destination path; if your command writes the signed package here, the workflow picks it up. If you modifyLGX_PATHin-place instead, that's fine too.
Optional signing_command_image runs the command inside a Docker image,
useful when the signing toolchain isn't on the default runner.
The client identifies your repository by the URL of a logos-repo.json
file. Put one at the root of your default branch (or wherever you prefer
— the URL is opaque to the client). Schema:
{
"schemaVersion": 1,
"name": "my-logos-modules",
"displayName": "My Modules",
"description": "...",
"homepage": "https://example.com/modules",
"indexUrl": "https://github.com/<owner>/<repo>/releases/download/index/index.json",
"trustedSigners": [
{ "did": "did:jwk:...", "name": "..." }
]
}This repo follows the standard "moving-major-tag" pattern:
@v1— moving tag pointing at the latest 1.x release.@v1.2.3— exact pin (recommended for production).main— bleeding edge; not for consumers.
Bumping v2 means a breaking change to the workflow's inputs or output
schema. Stay on @v1 until you've migrated.
MIT.