Skip to content

act workflow files

Nostr CI workflows use GitHub Actions syntax and run through act. Most files copied from .github/workflows/ work unchanged. This page lists exactly what the coordinator accepts, what it refuses, and what it adds, verified against the ngit-ci source named above. Behaviour that only the operator controls lives in the generated coordinator reference.

Where files live

text
.ngit/act/workflows/<name>.yml     # or .yaml

Files in .github/workflows/, .forgejo/workflows/, .gitea/workflows/, and .gitlab-ci.yml are detected but never executed. The separate directory lets one repository target Nostr CI without changing what its GitHub or Forgejo mirrors run, and lets you keep the two sets different when you want to.

A coordinator reads the files at the exact commit that triggered the run, and every result names the file by path and by the SHA-256 of its content at that commit.

Triggers

on: keySupportedNotes
pushYesFires when the signed repository state adds or moves a branch or tag
pull_requestYesFires on a Nostr PR or PR update; the PR head is the checkout
workflow_dispatchReplay onlyNot a trigger, but a manual run uses this event if the file declares it
scheduleNoNothing fires on a timer
Anything elseNoIgnored

A manual run, from ngit ci trigger or GitWorkshop's retry button, replays any file regardless of its on: clause. It runs under workflow_dispatch if declared, otherwise under push or pull_request, and its results say manual.

Ref filters

branches, branches-ignore, tags, and tags-ignore accept exact names, *, prefix*, and prefix/**. Combining an include and an ignore filter for the same ref type is invalid on GitHub and the coordinator treats it the same way: the workflow does not run. A push with no filters runs for every branch and tag; declaring only branches excludes tag pushes, and only tags excludes branch pushes.

Path filters

paths and paths-ignore use GitHub glob syntax (*, **, ?) on both triggers. Inside paths, ! patterns exclude files; positive and negative sets are matched independently, so order-sensitive re-inclusion is not supported, and a list with no valid positive pattern never matches.

Path filters are not evaluated for tag pushes. For a push, the changed set is the diff from the previous commit at that ref. For a PR it is the diff from the merge base with the target repository's default branch, taken from the signed repository state. When the changed set cannot be computed (first push of a branch, a force push that discarded the old commit, or a PR more than 256 commits from its base) the workflow runs as if it had no path filter.

runs-on decides who runs the file

A coordinator claims a workflow as a whole file, and only when it supports the runs-on labels of every job. A file nobody can serve is skipped silently: no run, no result, no error. That is deliberate, so a coordinator on the right platform can pick it up instead.

  • Labels must resolve statically: a string, a list, group.labels, or a matrix.<key> expression over a static matrix, including include entries. Any other expression makes coordinators skip the file.
  • A list is satisfied by any one label. Matching is case-insensitive.
  • ngit-ci's default labels are ubuntu-latest, ubuntu-24.04, and ubuntu-22.04. Operators may add others, such as ubuntu-24.04-arm.
  • macOS and Windows labels cannot be served. Jobs run in Linux containers.

For more than one architecture, use one file per architecture. A single file whose jobs span architectures runs only where one coordinator supports every label at once, and a fan-in job that needs both builds can never be split across machines.

Check what your coordinator advertises before choosing labels: Choose a coordinator.

Jobs

  • Independent jobs run in parallel, up to the executing host's CPU count.
  • needs: ordering and needs.<job>.outputs.* work as on GitHub.
  • A static strategy.matrix works; matrix values built from expressions do not.
  • There is no chaining across files or across runs.
  • The whole run is subject to the coordinator's timeout, 30 minutes by default. On expiry the workflow concludes timed_out and a diagnostic Job Result named ngit-ci timeout diagnostic (job id __ngit_ci_timeout__) carries the partial output. It is not a job you declared.

Refused by default

The operator owns the job container, so under the default policy these are refused, and the refusal is published as a startup_failure Workflow Result rather than a silent skip:

DeclarationDefault outcome
container: or services: with options or volumesRefused
Any container: or services: when the operator sets container optionsRefused
Job-level uses: (reusable workflows)Refused
Composite actions used from a stepAllowed

An operator who trusts workflow authors can switch to the workflow policy, which restores act's native behaviour.

The runtime environment

  • Jobs run in act's catthehacker/ubuntu:act-* images, not GitHub's runner VMs. Tools you expect may be missing or differ in version. Install what you need with a setup action or a package step.
  • Put actions/checkout before any step that reads the repository. It maps to the coordinator's checkout of the trigger commit, with Git metadata. Checking out a different private repository does not work.
  • Outbound network access is available. There is no container daemon, so docker commands fail unless the operator opted in.
  • These variables are set on every job:
VariableValue
GITHUB_SHAThe commit that ran
GITHUB_REFThe Git ref for a push, or refs/pull/ngit for a PR unless the trigger supplied one
NGIT_CI_REPOSITORYThe repository coordinate, 30617:<pubkey>:<identifier>
NGIT_CI_TRIGGER_EVENTThe id of the Nostr event that triggered the run

There is no GITHUB_TOKEN and github.token is empty. Actions that require a token need an explicit value; see the cache note below.

To use ngit or a nostr:// remote inside a job, add the setup action, which installs ngit and git-remote-nostr from a checksum-pinned manifest and also works on GitHub-hosted runners:

yaml
- uses: danconwaydev/setup-ngit@v3
  with:
    version: 3.0.0-rc.8    # optional

Secrets

The secrets context is populated only with names provisioned for this repository, and only when the run's trigger was authored by a confirmed maintainer. A third-party PR always runs with empty secrets, so a workflow that must work for contributors cannot depend on one. How maintainers and operators provision values is covered in Configure secrets.

Names match [A-Z_][A-Z0-9_]*. Names that collide with runtime variables are rejected, including PATH, HOME, CI, DOCKER_HOST, NIX_CONFIG, and anything starting GITHUB_, NGIT_CI_, RUNNER_, or ACTIONS_.

One name is special. If a maintainer provisions GH_READ_TOKEN, the coordinator also writes it into NIX_CONFIG as a GitHub access token for authorised runs, so locked flake inputs of type github fetch without a workflow change. It authenticates nothing else.

Artifacts

actions/upload-artifact@v4 and download-artifact work within one run. When the operator has enabled Blossom uploads, every file inside an uploaded artifact is published as its own artifact entry on the Job Result, with a Blossom URL that embeds the file's SHA-256. GitWorkshop lists them with a copyable hash, and a later release step can fetch and verify them. Uploads hidden inside a composite action need the upload step's outputs to stay visible, or the files are omitted rather than misattributed.

Public outputs and PR previews

Ordinary job outputs become public output entries on the Job Result:

yaml
jobs:
  preview:
    runs-on: ubuntu-latest
    outputs:
      nsite_preview: ${{ steps.publish.outputs.url }}

Only literal values and a direct steps.<id>.outputs.<name> expression resolve. Each value is limited to 8 KiB and a job to 64 KiB of outputs in total. An output that was declared but never set is published as omitted with a reason (missing, oversized, or unresolved) instead of an empty string. Output values are public Nostr tags: never put a credential in one.

Nsite preview convention. An output named nsite or starting nsite_ whose value is the URL of a successfully published nsite is recognised by GitWorkshop as a pull request preview and shown with an "Open nsite preview" button and an untrusted-content warning. Create a fresh Nostr identity inside the job rather than injecting a maintainer key; the signed CI result is the durable link between the PR, the commit, and that disposable identity. This site's own .ngit/act/workflows/docs.yml does exactly that with ngit account create --local followed by ngit nsite publish.

Caching

The coordinator runs an actions/cache-compatible server by default, scoped by repository and by trigger author's trust, so a third-party PR never reads or writes the maintainer cache. Use actions/cache@v4 or nix-community/cache-nix-action as usual, mark cache steps continue-on-error: true so a coordinator with caching disabled still runs the job, and give cache-nix-action a placeholder token: unused, because its default reads the empty github.token and aborts. Treat cache contents as an optimisation, never as an input to correctness or secrets.

Running the same workflow on GitHub

A coordinator runs only the workflow paths it advertises, under .ngit/<runner-format>/workflows/, so it never touches .github/workflows/ and the two systems run side by side. A check that must run on both exists in both places. Differences to expect when you copy a file across: no GITHUB_TOKEN, lighter images, refused container: blocks, and whole-file claiming by runs-on.

Nothing ran?

The usual causes, in order: no watching coordinator supports the file's runs-on labels, the trigger or its filters did not match, or runs-on could not be resolved statically. A file the coordinator refused produces a startup_failure result, not silence. See When no run appears.

Git collaboration, without the platform.