Actions for Forgejo runners because apparently they just cannot handle GitHub ones and I hate finding them
  • TypeScript 91.8%
  • JavaScript 3.6%
  • Shell 2.8%
  • Python 1.8%
Find a file
Zachary Handley 18de5b9f0f fix(stamp-version): the action declared three outputs and mapped none of them, so every caller read empty
A composite action's outputs are ONLY its declared `outputs.<name>.value`
expressions. A `$GITHUB_OUTPUT` write inside a composite step sets that INNER
step's output and never passes through unmapped. ZArcRunner implements exactly
that, and skips any output whose value is unset:

    // GHA parity: a composite action's outputs are ONLY the declared `outputs:`
    // mapping values (inner GITHUB_OUTPUT writes never pass through unmapped).
    for outputName, outputDef := range spec.Outputs {
        if outputDef.Value != "" { outputs[outputName] = ... }
    }

stamp-version declared `stamped`, `crates` and `verified` with a description and
no `value:`, so the map it returned was empty, the caller's
`if step.ID != "" && len(outputs) > 0` guard never fired, and
`steps.<id>.outputs.verified` was the empty string in every consumer.

Measured on BlazenQuant release run 32194: the action's own log printed
`verified=true  version=0.1.5  changed=3  publishable=3` and the runner then
recorded `parsed GITHUB_OUTPUT file ... keys:0` for that step, after which
`Assert the stamp verified` aborted with "did not report verified=true (got
'<empty>')". The action was working; only its result could not leave it. That is
fail-closed behaviour doing its job on a signal this file never sent — a false
red, not a false green, so nothing unverified was ever published.

Blast radius: every repo converted to this action AND asserting on its output —
BlazenQuant, BlazenModels, blazen-candle and blazen-weight-registry — could not
complete a release. Blazen calls the action but does not assert on `verified`,
so it was unaffected; the action still fails closed there by exit code.

Audited the whole repo rather than fixing the one report: of the 29 composite
actions here, stamp-version was the ONLY one with declared outputs and no
`value:` mapping. detect-markers and zregistry-token both carry theirs, which is
why the pattern was never noticed.

The three mappings are added with a note at the point of the defect recording
why they are load-bearing, so a future edit that drops one has to argue with it.
README gains the stamp-version section it never had.
2026-08-11 15:33:39 -07:00
.forgejo/workflows detect-markers: declarative custom-markers input + custom JSON output 2026-08-10 09:31:52 -07:00
ai-review chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
appstore-metadata-sync fix(upload-appstore, appstore-metadata-sync): honor RUNNER_USER_HOME 2026-05-26 20:47:50 -07:00
appstore-testflight Add appstore-testflight action 2026-05-24 11:51:19 -07:00
cargo-audit Swap to Public org 2026-03-03 13:20:35 -08:00
cargo-get Add action to build actions lmao 2026-04-14 14:41:24 -07:00
cargo-publish-workspace fix(cargo-publish-workspace): recognise every already-published conflict, and fail on yanked 2026-08-06 12:00:26 -07:00
channel-manifest-verify fix(channel-manifest-verify): an absent OS key no longer kills the step silently 2026-08-01 08:55:37 -07:00
check-appwrite-storage fix go setup 2026-05-26 10:30:11 -07:00
check-orphan-versions Use tmp branches for version detection and flagging rather than tags 2026-04-26 08:59:07 -07:00
ci-step-status ci-step-status + detect-changes: support diff-driven test skip 2026-05-28 13:09:59 -07:00
detect-changes ci-step-status + detect-changes: support diff-driven test skip 2026-05-28 13:09:59 -07:00
detect-markers detect-markers: declarative custom-markers input + custom JSON output 2026-08-10 09:31:52 -07:00
dispatch-forgejo-workflow Add dispatch-forgejo-workflow action 2026-05-24 11:17:00 -07:00
download-artifact up/download-artifact: --no-same-owner on tar extract (rootless remapped-root safe) 2026-08-04 10:01:45 -07:00
download-file chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
download-resumable feat(download-resumable): composite curl download with stall detection + byte-offset resume 2026-06-12 16:55:43 -07:00
e2e-image-smoke e2e-image-smoke: add smoke-entrypoint mode for shell-less (distroless) images 2026-07-24 17:01:18 -07:00
examples Add canonical release-mobile.yml example + index new mobile actions 2026-05-24 12:58:55 -07:00
github_format fix: fixed /tmp staging paths are shared by every job on the host 2026-08-04 14:17:34 -07:00
golangci-lint chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
google-play-metadata-sync Add google-play-metadata-sync action 2026-05-24 12:53:06 -07:00
google-play-staged-rollout Add google-play-staged-rollout action 2026-05-24 12:53:06 -07:00
helm-unittest helm-unittest: migrate to node20 bundle 2026-04-15 15:08:33 -07:00
install-git-bash chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
intellitester chore: sync intellitester action.yml from Intellitester@c00669666ef0082f6f34f48379194a5d566f45c8 2026-05-11 18:55:46 +00:00
komodo-redeploy komodo-redeploy: composite action that fires per-stack Komodo deploy webhook 2026-05-28 15:19:15 -07:00
mint-appwrite-token mint-appwrite-token: mint per-file storage tokens via Appwrite Tokens API 2026-05-29 00:26:20 -07:00
npm-publish-workspace fix: fixed /tmp staging paths are shared by every job on the host 2026-08-04 14:17:34 -07:00
prune-appwrite-storage fix go setup 2026-05-26 10:30:11 -07:00
pypi-publish-workspace Fix python urls 2026-04-23 10:43:33 -07:00
release-continue release-continue: retry git push + coordinator dispatch on transient DNS/network 2026-07-24 23:47:43 -07:00
release-orchestrator feat(release-orchestrator): carry the [purge] decision in release state 2026-08-01 11:22:10 -07:00
release-service release-service: migrate to node20 bundle 2026-04-15 14:55:25 -07:00
resolve-version stop resolve-version treating an auth failure as an empty registry 2026-08-04 08:03:59 -07:00
ruby-gem-publish feat(release): reusable composite actions — release-continue (mark+commit+re-dispatch), npm-publish-workspace, ruby-gem-publish 2026-07-23 20:18:25 -07:00
s3-cache chore: rebuild action bundles 2026-07-31 06:48:39 +00:00
setup-android-sdk chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-binaryen chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-bun chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-ccache chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-container-runtime Add setup-container-runtime (composite) and verify-artifact (node20) actions 2026-05-15 17:38:17 -07:00
setup-cuda-toolkit Add setup-cuda-toolkit action 2026-05-26 18:08:10 -07:00
setup-deno chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-docker chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-dotnet chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-fastlane chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-flutter chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-go chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-gradle chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-helm chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-java chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-kind chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-kotlin chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-kubectl chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-libclang chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-msvc chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-ninja chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-node chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-php fix: fixed /tmp staging paths are shared by every job on the host 2026-08-04 14:17:34 -07:00
setup-pnpm Add setup-pnpm 2026-05-11 13:45:21 -07:00
setup-podman chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-python chore: rebuild action bundles 2026-07-05 04:16:09 +00:00
setup-ruby chore: rebuild action bundles 2026-07-31 06:48:39 +00:00
setup-rust chore: rebuild action bundles 2026-08-10 21:53:45 +00:00
setup-sccache chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-shell chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-swift chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-system-deps chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-tauri-cli chore: rebuild action bundles 2026-07-05 04:16:09 +00:00
setup-terraform chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
setup-uniffi-bindgen-go Fix bc ai is dum 2026-05-20 23:02:07 -07:00
setup-wasi-sdk chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-zgit setup-zgit: source-build fallback when a release lacks the host's asset 2026-08-06 18:45:29 -07:00
setup-zig chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
setup-ztus chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
shared fix(platform): bun run typecheck has been red on main since the tar test landed 2026-07-31 02:48:25 -04:00
sonarqube chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
stamp-version fix(stamp-version): the action declared three outputs and mapped none of them, so every caller read empty 2026-08-11 15:33:39 -07:00
submit-appstore-review Add submit-appstore-review action 2026-05-24 11:51:19 -07:00
tauri-build-android chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
tauri-build-ios chore: rebuild action bundles 2026-06-26 17:12:04 +00:00
trivy chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
tus-upload chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
upload-appstore chore: rebuild action bundles 2026-07-25 00:24:39 +00:00
upload-appwrite-storage Fix Queries for upload 2026-04-15 08:58:36 -07:00
upload-artifact up/download-artifact: --no-same-owner on tar extract (rootless remapped-root safe) 2026-08-04 10:01:45 -07:00
upload-firebase-app-distribution Add upload-firebase-app-distribution action 2026-05-24 11:17:00 -07:00
upload-google-play upload-google-play: add staged rollout, release notes, mapping file 2026-05-24 12:53:06 -07:00
verify-artifact verify-artifact: probe with GET+Range, not HEAD (Forgejo returns 405) 2026-05-18 19:11:56 -07:00
windows-sign-binaries fix(windows-sign-binaries): cross-platform (bash) — download prebuilt on Windows, cargo install on Linux for cross-compiled PEs 2026-08-10 20:38:49 -07:00
windows-trusted-signing fix(win-signing): use prebuilt artifact-signing-cli; add windows-sign-binaries for raw exes 2026-08-10 20:33:19 -07:00
wrangler-deploy wrangler-deploy: composite action wrapping wrangler CLI 2026-05-31 18:44:46 -07:00
zgit-workspace zgit-workspace: use setup-shell for the bash bootstrap 2026-07-30 18:08:06 -07:00
zlayer-build-push zlayer-build-push: symlink zlayer into /usr/local/bin as PATH belt-and-suspenders 2026-07-20 22:35:07 -04:00
zregistry-check feat(zregistry): add composite actions (setup/publish/download/grant/revoke/check) 2026-06-04 08:51:31 -07:00
zregistry-download feat(zregistry): add composite actions (setup/publish/download/grant/revoke/check) 2026-06-04 08:51:31 -07:00
zregistry-grant feat(zregistry): add composite actions (setup/publish/download/grant/revoke/check) 2026-06-04 08:51:31 -07:00
zregistry-publish fix(zregistry-publish): use zregistry-setup's cli-path output, not bare PATH 2026-07-24 13:16:40 -07:00
zregistry-revoke feat(zregistry): add composite actions (setup/publish/download/grant/revoke/check) 2026-06-04 08:51:31 -07:00
zregistry-setup fix(zregistry-setup): tar --no-same-owner so extract works on rootless runners 2026-07-26 17:53:42 -07:00
zregistry-token fix(zregistry-token): default scope to packages.publish so minted tokens carry the write scope ZRegistry gates on (empty no longer mints unscoped); also keyless token-exchange mode 2026-06-06 18:50:40 -07:00
zrelay-notify Add zrelay-notify action 2026-05-24 11:17:46 -07:00
.gitattributes Add action to build actions lmao 2026-04-14 14:41:24 -07:00
.gitignore Add action to build actions lmao 2026-04-14 14:41:24 -07:00
bun.lock fix(platform): bun run typecheck has been red on main since the tar test landed 2026-07-31 02:48:25 -04:00
LICENSE Initial commit 2025-11-30 21:31:00 +00:00
package.json setup-binaryen: install wasm-opt/binaryen from GitHub releases 2026-05-27 16:43:51 -07:00
README.md fix(stamp-version): the action declared three outputs and mapped none of them, so every caller read empty 2026-08-11 15:33:39 -07:00
tsconfig.base.json Add action to build actions lmao 2026-04-14 14:41:24 -07:00

actions

Actions for Forgejo runners because apparently they just cannot handle GitHub ones and I hate finding them

Language Setup Actions

setup-go

Install Go from official releases.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-go@main
  with:
    go-version: '1.23'    # optional, default: 1.23
    cache: true           # optional, default: true

setup-node

Install Node.js with package manager support.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-node@main
  with:
    node-version: '22'           # optional, default: 22
    package-manager: pnpm        # optional: npm, yarn, pnpm (default: npm)
    cache: true                  # optional, default: true

setup-python

Install Python via uv (fast Python package manager).

- uses: https://forge.blackleafdigital.com/Public/actions/setup-python@main
  with:
    python-version: '3.12'    # optional, default: 3.12
    uv-version: latest        # optional, default: latest
    cache: true               # optional, default: true

setup-rust

Install Rust toolchain via rustup with optional sccache integration.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-rust@main
  with:
    toolchain: stable              # optional, default: stable
    components: rustfmt, clippy    # optional
    targets: x86_64-unknown-linux-musl  # optional

With sccache + S3

- uses: https://forge.blackleafdigital.com/Public/actions/setup-rust@main
  with:
    components: rustfmt, clippy
    sccache: true
    sccache-s3-bucket: ${{ secrets.S3_BUCKET }}
    sccache-s3-endpoint: https://${{ secrets.S3_ENDPOINT }}
    sccache-s3-region: ${{ secrets.S3_REGION }}
    sccache-s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
    sccache-s3-secret-access-key: ${{ secrets.S3_ACCESS_SECRET }}
    sccache-key-prefix: sccache/myproject

setup-bun

Install Bun JavaScript runtime.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-bun@main
  with:
    bun-version: latest    # optional, default: latest

setup-deno

Install Deno JavaScript/TypeScript runtime.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-deno@main
  with:
    deno-version: latest    # optional, default: latest

setup-java

Install Java/JDK via Eclipse Temurin (Adoptium).

- uses: https://forge.blackleafdigital.com/Public/actions/setup-java@main
  with:
    java-version: '21'        # optional, default: 21
    distribution: temurin     # optional, default: temurin

setup-dotnet

Install .NET SDK via official Microsoft installer.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-dotnet@main
  with:
    dotnet-version: '8.0'    # optional, default: 8.0

setup-zig

Install Zig compiler.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-zig@main
  with:
    zig-version: '0.13.0'    # optional, default: 0.13.0 (or 'master')

setup-swift

Install Swift toolchain.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-swift@main
  with:
    swift-version: '5.10'    # optional, default: 5.10

Tool Setup Actions

setup-kind

Install Kind (Kubernetes in Docker) with optional cluster creation.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-kind@main
  with:
    kind-version: v0.24.0     # optional, default: v0.24.0
    cluster-name: kind        # optional, default: kind
    wait: '120s'              # optional, default: 120s
    create-cluster: true      # optional, default: true

setup-kubectl

Install kubectl CLI.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-kubectl@main
  with:
    kubectl-version: stable    # optional, default: stable (latest)

setup-helm

Install Helm package manager for Kubernetes.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-helm@main
  with:
    helm-version: latest    # optional, default: latest

setup-terraform

Install Terraform from HashiCorp.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-terraform@main
  with:
    terraform-version: latest    # optional, default: latest

setup-docker

Install Docker (Linux only, macOS requires Docker Desktop).

- uses: https://forge.blackleafdigital.com/Public/actions/setup-docker@main
  with:
    docker-version: latest    # optional, default: latest

setup-podman

Install Podman container engine.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-podman@main
  with:
    podman-version: latest    # optional, default: latest (system package)

setup-sccache

Standalone sccache setup with optional S3 backend.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-sccache@main
  with:
    version: v0.8.1                # optional
    bucket: ${{ secrets.S3_BUCKET }}
    endpoint: https://${{ secrets.S3_ENDPOINT }}
    region: ${{ secrets.S3_REGION }}
    access-key: ${{ secrets.S3_ACCESS_KEY }}
    secret-key: ${{ secrets.S3_ACCESS_SECRET }}
    key-prefix: sccache/myproject

setup-zgit

Download the prebuilt zgit CLI from a zGit release and add it to PATH. Resolves latest (or a pinned v tag) via the Forgejo releases API and selects the bare binary for the runner OS+arch (x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-pc-windows-msvc.exe). zGit is a private repo, so a Forgejo token is required for both the release lookup and the asset download.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-zgit@main
  with:
    version: latest                              # optional, default: latest (or a tag like v0.0.12)
    forgejo-url: https://forge.blackleafdigital.com   # optional
    repo: BlackLeafDigital/zGit                  # optional
    token: ${{ secrets.FORGEJO_TOKEN }}          # required: zGit is private
# outputs: cli-path, resolved-tag

zgit-workspace

Put a zGit Project's members on disk in one step: installs zgit (via setup-zgit), hard-verifies the super-repo is at the run commit, then zgit project sync --frozen. Replaces the hand-rolled materialize/verify shell scripts every zGit-Project repo used to duplicate.

- uses: actions/checkout@v4
- uses: https://forge.blackleafdigital.com/Public/actions/zgit-workspace@main
  with:
    token: ${{ secrets.FORGEJO_TOKEN }}          # required: member repos are private
    sha: ${{ github.sha }}                       # optional, default: github.sha
    working-directory: .                         # optional, default: . (dir holding .zproject.toml)
    version: latest                              # optional, default: latest
    forgejo-url: https://forge.blackleafdigital.com   # optional
    repo: BlackLeafDigital/zGit                  # optional
    heal-global-git-config: 'true'               # optional, default: true
# outputs: member-count, declared-count, sync-json, zgit-version

Fails the build (never warns) when: the token is empty, git rev-parse HEAD is not sha — the stale-warm-workspace failure mode that once shipped a binary built from a 19-commit-old tree — .zproject.lock is missing (--frozen requires it), or fewer members land than .zproject.toml declares.

--frozen clones absent members so a cold runner works, never pulls present ones so a warm runner does no network I/O, and treats any lock/pin drift as a hard error. The token is passed through git's GIT_CONFIG_COUNT env protocol — never in a URL, in argv, or in a persisted config file — and is scoped to the forge host only.

Windows: every run: step here is shell: bash. On a Windows host executor with no bash on PATH, run https://forge.blackleafdigital.com/Public/actions/install-git-bash@main first — otherwise every step of this action fails to launch.

Security & Quality Actions

cargo-get

Extract metadata (version, name, description, authors, etc.) from Cargo.toml without needing Rust installed. Handles workspace inheritance ({ workspace = true } fields).

- uses: https://forge.blackleafdigital.com/Public/actions/cargo-get@main
  with:
    path: .                    # optional, default: .  (dir or Cargo.toml path)
    workspace-member: mycrate  # optional: specific workspace member to query

Outputs include package_name, package_version, package_version_{major,minor,patch,pre,build}, package_edition, package_rust_version, package_authors, package_description, package_license, package_publish, workspace_members, is_workspace, and more.

cargo-publish-workspace

Idempotent, rate-limit-aware publisher for Rust workspaces. Skips crates already live at the target version on crates.io and retries each crate with per-crate exponential backoff (up to 5 minutes) on HTTP 429. Publishes in topological dependency order.

- uses: https://forge.blackleafdigital.com/Public/actions/cargo-publish-workspace@main
  with:
    version: ${{ needs.version.outputs.version }}
    registry: crates-io                                   # optional, default: crates-io
    exclude: blazen-py,blazen-node                        # optional: comma-separated skip list
    cargo-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
    max-backoff-seconds: '300'                            # optional, default: 300
    max-attempts-per-crate: '5'                           # optional, default: 5

Requires setup-rust to have run earlier in the job. Outputs published, skipped, total (comma-separated lists + count).

cargo-audit

Run security audit for Rust dependencies.

- uses: https://forge.blackleafdigital.com/Public/actions/cargo-audit@main
  with:
    deny: unmaintained  # optional: fail on unmaintained crates

trivy

Run Trivy security scanner for vulnerabilities in code, containers, and dependencies.

- uses: https://forge.blackleafdigital.com/Public/actions/trivy@main
  with:
    scan-type: fs                    # optional: fs, image, repo (default: fs)
    severity: CRITICAL,HIGH          # optional (default: CRITICAL,HIGH)
    exit-code: '1'                   # optional: fail on findings (default: 1)
    ignore-unfixed: false            # optional: ignore unpatched vulns
    format: table                    # optional: table, json, sarif

Scan Docker image

- uses: https://forge.blackleafdigital.com/Public/actions/trivy@main
  with:
    scan-type: image
    image-ref: myapp:latest
    severity: CRITICAL

sonarqube

Run SonarQube code quality analysis.

- uses: https://forge.blackleafdigital.com/Public/actions/sonarqube@main
  with:
    host-url: ${{ secrets.SONAR_HOST_URL }}
    token: ${{ secrets.SONAR_TOKEN }}
    project-key: my-project
    project-name: My Project         # optional (defaults to project-key)
    sources: src                     # optional (default: .)
    exclusions: '**/*.test.ts'       # optional: patterns to exclude

ai-review

AI-powered code review using Z.AI (GLM) or other OpenAI-compatible providers.

- uses: https://forge.blackleafdigital.com/Public/actions/ai-review@main
  with:
    api-key: ${{ secrets.ZAI_API_KEY }}
    forgejo-token: ${{ secrets.FORGEJO_TOKEN }}
    forgejo-url: https://forge.blackleafdigital.com
    model: glm-4.6                   # optional (default: glm-4.6)
    base-url: https://api.z.ai/v1    # optional (default: Z.AI)
    provider: openai                 # optional: openai, anthropic, ollama

With OpenAI

- uses: https://forge.blackleafdigital.com/Public/actions/ai-review@main
  with:
    api-key: ${{ secrets.OPENAI_API_KEY }}
    forgejo-token: ${{ secrets.FORGEJO_TOKEN }}
    forgejo-url: https://forge.blackleafdigital.com
    base-url: https://api.openai.com/v1
    model: gpt-4o

Mobile App Release

End-to-end Tauri 2.x mobile release pipeline — toolchain setup, signed builds, store uploads, TestFlight / staged rollouts, metadata sync, and App Review submission. See examples/release-mobile.yml for the canonical workflow that chains these actions together (parallel iOS + Android jobs with ci-step-status gating, zrelay-notify on success/failure, and a Forgejo release at the end).

setup-fastlane

Install fastlane (and bundler) onto the runner. Requires Ruby on PATH (run setup-ruby first). Detects a Gemfile and falls back to gem install fastlane when none is present.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-ruby@main
  with:
    ruby-version: '3.3'
- uses: https://forge.blackleafdigital.com/Public/actions/setup-fastlane@main
  with:
    fastlane-version: latest    # optional, default: latest
    bundler-version: latest     # optional, default: latest
    gemfile-dir: .              # optional, default: .

setup-tauri-cli

Install the Tauri 2.x CLI via cargo-binstall (preferred, prebuilt) or cargo install, with optional iOS / Android Rust target installation.

- uses: https://forge.blackleafdigital.com/Public/actions/setup-tauri-cli@main
  with:
    tauri-version: latest    # optional, default: latest
    target: ios              # optional: desktop | ios | android | all
    use-prebuilt: true       # optional, default: true (prefer cargo-binstall)

tauri-build-ios

Build a Tauri 2.x iOS app and produce a signed IPA ready for App Store / TestFlight / ad-hoc distribution. Outputs ipa-path, bundle-version, and bundle-short-version.

- uses: https://forge.blackleafdigital.com/Public/actions/tauri-build-ios@main
  with:
    bundle-id: com.blackleaf.imaige
    code-sign-identity: ${{ secrets.IOS_CODE_SIGN_IDENTITY }}
    provisioning-profile-name: ${{ secrets.IOS_PROVISIONING_PROFILE_NAME }}
    team-id: ${{ secrets.IOS_TEAM_ID }}
    target: aarch64-apple-ios    # optional, default: aarch64-apple-ios
    export-method: app-store     # optional: app-store | ad-hoc | enterprise | development

tauri-build-android

Build a signed Android AAB (and optional APK) from a Tauri 2.x project using the Tauri CLI + Gradle. Decodes a base64 keystore on the fly. Outputs aab-path, apk-path, version-code, version-name.

- uses: https://forge.blackleafdigital.com/Public/actions/tauri-build-android@main
  with:
    package-name: com.blackleaf.imaige
    keystore-base64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
    keystore-password: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
    key-alias: ${{ secrets.ANDROID_KEY_ALIAS }}
    key-password: ${{ secrets.ANDROID_KEY_PASSWORD }}
    targets: aarch64-linux-android    # optional, CSV of Android Rust targets
    build-apk: false                  # optional: also produce a signed APK

upload-appstore

Upload an IPA to App Store Connect (TestFlight) using API-key authentication. With wait-for-processing: true it polls App Store Connect until the build reaches VALID (or fails) and exposes build-id + processing-state for downstream TestFlight / review steps. Always emits bundle-version and bundle-short-version parsed from the IPA.

- uses: https://forge.blackleafdigital.com/Public/actions/upload-appstore@main
  with:
    ipa-path: ${{ steps.build-ios.outputs.ipa-path }}
    api-key-id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
    issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
    api-private-key: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }}
    wait-for-processing: 'true'    # optional, default: false
    timeout-minutes: '60'          # optional, default: 60
    poll-interval-seconds: '30'    # optional, default: 30

appstore-testflight

Attach a previously-uploaded build to TestFlight beta groups, set what-to-test localizations, toggle auto-notify, and optionally submit for external beta review.

- uses: https://forge.blackleafdigital.com/Public/actions/appstore-testflight@main
  with:
    api-key-id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
    issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
    api-private-key: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }}
    bundle-id: com.blackleaf.imaige
    build-version: ${{ steps.build-ios.outputs.bundle-version }}
    beta-groups: 'Internal QA,External Beta'
    submit-for-beta-review: auto    # optional: true | false | auto

appstore-metadata-sync

Sync App Store Connect metadata, screenshots, and app preview videos via fastlane deliver. Requires setup-fastlane to have run earlier in the job. Reads the standard fastlane/metadata + fastlane/screenshots directory layout.

- uses: https://forge.blackleafdigital.com/Public/actions/appstore-metadata-sync@main
  with:
    api-key-id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
    issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
    api-private-key: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }}
    app-identifier: com.blackleaf.imaige
    version-string: 1.2.3            # optional: bump to / target this version
    skip-screenshots: false          # optional, default: false
    skip-metadata: false             # optional, default: false

submit-appstore-review

Submit an App Store Connect version for review via the App Store Connect API. Supports auto-release on approval, phased rollout, and per-locale "what's new" text.

- uses: https://forge.blackleafdigital.com/Public/actions/submit-appstore-review@main
  with:
    api-key-id: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
    issuer-id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
    api-private-key: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY }}
    bundle-id: com.blackleaf.imaige
    version-string: 1.2.3
    build-version: ${{ steps.build-ios.outputs.bundle-version }}
    auto-release: 'true'    # optional, default: true (AFTER_APPROVAL)
    phased-release: 'false' # optional, default: false (7-day phased rollout)

upload-google-play

Upload an AAB to Google Play via the Developer API using a service account. Supports release-track selection, staged rollout with a user-fraction, per-locale release notes (JSON), and optional ProGuard/R8 mapping file upload after the bundle upload.

- uses: https://forge.blackleafdigital.com/Public/actions/upload-google-play@main
  with:
    aab-path: ${{ steps.build-android.outputs.aab-path }}
    package-name: com.blackleaf.imaige
    service-account-json: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
    track: internal               # optional: internal | alpha | beta | production
    user-fraction: '0.10'         # optional: start a staged rollout at 10%
    release-notes-json: '[{"language":"en-US","text":"Bug fixes."}]'  # optional
    mapping-file: app/build/outputs/mapping/release/mapping.txt        # optional

google-play-staged-rollout

Adjust an existing Google Play release's staged-rollout userFraction and status (set / halt / resume / complete) without re-uploading the bundle. Useful for promoting an internal upload to a wider rollout or pausing a rollout that's misbehaving.

- uses: https://forge.blackleafdigital.com/Public/actions/google-play-staged-rollout@main
  with:
    service-account-json: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
    package-name: com.blackleaf.imaige
    track: production
    action: set            # set | halt | resume | complete
    user-fraction: '0.10'  # required for set / resume

google-play-metadata-sync

Sync per-locale Google Play store listings (title, short / full description, video URL) and image assets (icon, feature graphic, phone / tablet / TV / wear screenshots) via the Play Developer API v3. Reads the fastlane/metadata/android/<locale>/... directory layout.

- uses: https://forge.blackleafdigital.com/Public/actions/google-play-metadata-sync@main
  with:
    service-account-json: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
    package-name: com.blackleaf.imaige
    listings-dir: ./fastlane/metadata/android    # optional, default shown
    image-types: 'phoneScreenshots,featureGraphic,icon'  # optional

zrelay-notify

POST a notification to the BlackLeafDigital ZRelay notification relay. Supports SMS, email, Discord, iMessage, FCM push, web push, and APNs channels. Handy as the final step of a release pipeline to ping the team on success / failure.

- uses: https://forge.blackleafdigital.com/Public/actions/zrelay-notify@main
  with:
    api-key: ${{ secrets.ZRELAY_API_KEY }}
    channel: email                       # sms | email | discord | imessage | push | webpush | apns
    recipient: ${{ secrets.ZRELAY_NOTIFY_RECIPIENT }}
    subject: "Release v${{ inputs.version }} shipped"
    body: |
      iOS + Android builds uploaded successfully.
    fail-on-error: 'true'    # optional, default: true

CI / Release Orchestration

detect-markers

Parse commit-message markers into job-gating outputs. Single source of truth for the house marker convention — replaces the inline contains(...)/bash that every repo used to duplicate.

- id: m
  uses: https://forge.blackleafdigital.com/Public/actions/detect-markers@main
  with:
    message: ${{ github.event.head_commit.message }}   # push event
    # message: ${{ github.event.workflow_run.head_commit.message }}  # workflow_run
    # default-bump: patch                               # optional
    # subject-only: true                                # optional — see below

⚠️ The body arms the marker — and subject-only: is the fix

Every marker test is a substring/regex match over the whole string handed to the action. When message: is empty the action falls back to git log -1, and that fallback defaults to --pretty=%Bsubject and body. So a commit whose body merely mentions a marker as prose arms it.

This is not theoretical. A Blazen commit that documented [release], [purge] and [v0.8.0] in its body parsed as version=0.8.0 purge=true; only the [skip] in its subject held release at false. That repo's release chain walks itself to a crates.io publish, which is permanent and un-replaceable.

    subject-only: true    # fallback reads `--pretty=%s` (subject) not `%B`
  • Default is falsetoday's exact behaviour, unchanged for every existing caller. This input is purely opt-in.
  • It affects only the git log fallback. A non-empty message: is always parsed verbatim, whatever subject-only is set to — so a caller that passes ${{ github.event.head_commit.message }} (the full message, body included) gets no protection from this input; it must pass a subject instead, or switch to message: '' + subject-only: true.
  • Truthy spellings: true/yes/on/1, any case. Anything else is false, so a typo fails open to today's behaviour, never accidentally-on.
  • The step log names the source it parsed (input / %s / %B), so a run is self-evidencing.

The message: '' trap. The fallback is keyed on the value being empty, not on the input being absent — so an explicitly-passed message: '' re-opens it. That is the documented ZLayer auto-tag.yaml idiom (it is how a dispatched workflow re-derives markers off the checked-out sha), and it is deliberately preserved. If you use it, pair it with subject-only: true.

Regardless of any of the above, the standing rule holds: never write a bracketed marker literally in a commit message you do not intend to fire — not even in the body. Break it up ([re + lease]) or spell it out.

Marker Output Meaning
[release] (or [release minor] / [release:major]) release=true opt into the publish/release chain
[np] / [no-publish] no-publish=truerelease=false suppress publishing
[skip] / [skip-checks] / [skip-ci] / [skip ci] / [ci skip] skip=truerelease=false skip CI/checks
[fast] fast=true skip test/lint/audit jobs; release still fires
[purge] purge=true force fresh rebuild / invalidate cached artifacts
[major] / [minor] / [patch] bump=<x> bump type (default patch)
[vX.Y.Z] (opt. -pre) version=X.Y.Z[-pre] explicit version override
push event + release dry-run=false live publish; otherwise true

Outputs: release, no-publish, skip, fast, purge, bump, version, dry-run.

Canonical marker-driven release pattern (push-triggered ci.yml → dispatch release.yml, since Forgejo's on: workflow_run hook fires unreliably):

  dispatch-release:
    runs-on: ubuntu-latest
    needs: [lint, test]            # gate on green CI
    if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - id: m
        uses: https://forge.blackleafdigital.com/Public/actions/detect-markers@main
        with:
          message: ${{ github.event.head_commit.message }}
      - if: steps.m.outputs.release == 'true'
        uses: https://forge.blackleafdigital.com/Public/actions/dispatch-forgejo-workflow@main
        with:
          workflow: release.yml
          ref: main
          token: ${{ secrets.FORGEJO_ACTIONS_TOKEN }}
          inputs-json: >-
            {"version":"${{ steps.m.outputs.version }}","bump":"${{ steps.m.outputs.bump }}","dry_run":"false"}

stamp-version

Rewrite the 0.0.0-dev working-tree placeholder to a real release version across every Cargo.toml in a checkout, then verify it landed out of cargo metadata — failing the run closed if it did not. Wraps the canonical stamper (Blazen/scripts/stamp-version.py), fetched at run time so there is exactly one copy in the estate.

- id: stamp
  uses: https://forge.blackleafdigital.com/Public/actions/stamp-version@main
  with:
    version: ${{ steps.v.outputs.version }}    # BARE semver, no leading `v`
    repo-path: '.'                             # optional, default '.'
    mode: zreg                                 # zreg | crates-io | forgejo
    expected-publish: zreg                     # optional but STRONGLY recommended
    strip-git-dev-deps: 'true'                 # optional, default 'false'
    forgejo-token: ${{ secrets.FORGEJO_ACTIONS_TOKEN }}
# then: cargo-publish-workspace, which owns registry existence checking

⚠️ The stamper does not fail closed — this action is what does

Run stamp-version.py against a tree containing no 0.0.0-dev at all and it prints stamped version=<V> (...) and exits 0 having changed nothing. It also validates nothing about the version it is handed: stamp-version.py "v1.2.3-NOT-A-SEMVER!!" exits 0 and writes that literal into version = .

So rc=0 from the stamper proves only that it ran. The cargo metadata assertion is the gate, and leaving it to callers is exactly how the two divergent inline stamps of 2026-08-05 came to exist. This action refuses a non-semver, a leading v, and the 0.0.0-dev placeholder before stamping, and afterwards asserts every publishable member's version, every member's publish allowlist (when expected-publish is set), and that no dependency requirement anywhere still carries 0.0.0-dev.

  • verified is "true" only on success, "false" when verification ran and failed, and empty when the run died earlier — so test == 'true', never != 'false'.
  • Assertions are scoped to members whose manifest lives under repo-path. cargo metadata walks upwards to find a workspace root, so inside a meta-workspace member it otherwise reports the whole superproject (measured: inside ZataTypes it returns 55 members).
  • Run it after the registry credentials are written, and let it inherit the same CARGO_HOME: a dep naming an unconfigured registry makes cargo metadata exit 101.
  • It never contacts a registry. Whether a version is already live is cargo-publish-workspace's job and it already owns it.

dispatch-forgejo-workflow

Fire a Forgejo workflow_dispatch via the REST API (reliable replacement for the flaky on: workflow_run cross-workflow trigger). Optionally polls for completion.

- uses: https://forge.blackleafdigital.com/Public/actions/dispatch-forgejo-workflow@main
  with:
    workflow: release.yml          # required — workflow filename
    ref: main                      # optional, default: main
    inputs-json: '{"dry_run":"false"}'   # optional JSON of inputs
    token: ${{ secrets.FORGEJO_ACTIONS_TOKEN }}   # required
    wait: 'false'                  # optional — poll until run completes

ZRegistry Actions

Publish, install, and license-manage packages on a self-hosted ZRegistry. The setup/publish/grant/ revoke/check actions wrap the prebuilt zregistry admin CLI; zregistry-download fetches artifacts over each ecosystem's native HTTP route (no CLI download subcommand exists). ZRegistry is a private repo, so the CLI-installing actions need a Forgejo token (forgejo-token) to fetch the release asset.

Release-asset dependency: zregistry-setup downloads zregistry-{target}.tar.gz from a ZRegistry release, where the CLI binary inside is named zregistry-cli-{target}. The ZRegistry CI/release job currently builds these tarballs (x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu) and uploads them as CI artifacts — the release pipeline must also attach them as release assets for these actions to resolve them via the releases API.

zregistry-setup

Download the prebuilt zregistry CLI from a ZRegistry release and add it to PATH. Resolves latest (or a pinned tag) via the Forgejo releases API and selects the asset for the runner arch (x86_64 / aarch64).

- uses: https://forge.blackleafdigital.com/Public/actions/zregistry-setup@main
  with:
    version: latest                              # optional, default: latest (or a tag like v1.2.3)
    forgejo-url: https://forge.blackleafdigital.com   # optional
    repo: BlackLeafDigital/ZRegistry             # optional
    token: ${{ secrets.FORGEJO_TOKEN }}          # required: ZRegistry is private
# outputs: cli-path, resolved-tag

zregistry-publish

Install the CLI and zregistry publish an artifact. Maps inputs → flags; the publish token travels via ZREGISTRY_TOKEN.

- uses: https://forge.blackleafdigital.com/Public/actions/zregistry-publish@main
  with:
    ecosystem: npm                               # required
    file: ./dist/core-1.2.3.tgz                  # required
    name: '@ztana/core'                          # required for npm + generic
    version: '1.2.3'                             # required for generic
    registry-url: https://registry.blackleafdigital.com   # optional
    token: ${{ secrets.ZREGISTRY_TOKEN }}        # required (→ ZREGISTRY_TOKEN)
    forgejo-token: ${{ secrets.FORGEJO_TOKEN }}  # to fetch the private CLI asset
# outputs: status, output

zregistry-download

Fetch a package artifact via the ecosystem-native HTTP route (npm tarball, pypi file, cargo …/download, or generic /generic/{name}/{version}/{filename}). The token is sent in each ecosystem's native auth slot — npm → Bearer, pypi/cargo/ generic → HTTP Basic __token__:<token> (per ZRegistry/docs/installing.md).

- uses: https://forge.blackleafdigital.com/Public/actions/zregistry-download@main
  with:
    ecosystem: generic                           # optional, default: generic
    package: my-app                              # required
    version: '1.0.0'                             # default: latest (npm only resolves latest)
    filename: my-app-1.0.0.bin                   # required for generic route
    output-dir: ./downloads                      # optional, default: .
    registry-url: https://registry.blackleafdigital.com   # optional
    token: ${{ secrets.ZREGISTRY_TOKEN }}        # license key / JWT for non-public packages
# outputs: artifact-path, http-status

zregistry-grant

Grant package access via ZBilling — a SKU bundle (mode: skugrant-sku) or a per-license grant (mode: licensegrant-license).

- uses: https://forge.blackleafdigital.com/Public/actions/zregistry-grant@main
  with:
    mode: license                                # sku | license
    ecosystem: npm                               # required
    scope: '@ztana/*'                            # required (package glob)
    license: ${{ secrets.CUSTOMER_LICENSE }}     # required when mode=license
    # sku: pkg_zql_pro                           # required when mode=sku
    note: 'complimentary access'                 # optional (mode=license)
    zbilling-url: https://billing.blackleafdigital.com    # required
    zbilling-token: ${{ secrets.ZBILLING_ENTITLEMENTS_TOKEN }}   # required
    forgejo-token: ${{ secrets.FORGEJO_TOKEN }}  # to fetch the private CLI asset
# outputs: output

zregistry-revoke

Revoke a ZBilling license (zregistry revoke --license [--reason]).

- uses: https://forge.blackleafdigital.com/Public/actions/zregistry-revoke@main
  with:
    license: ${{ secrets.CUSTOMER_LICENSE }}     # required
    reason: 'refund'                             # optional
    zbilling-url: https://billing.blackleafdigital.com    # required
    zbilling-token: ${{ secrets.ZBILLING_ENTITLEMENTS_TOKEN }}   # required
    forgejo-token: ${{ secrets.FORGEJO_TOKEN }}  # to fetch the private CLI asset
# outputs: output

zregistry-check

Check whether a license may install a package (zregistry check). Surfaces the {allowed, reason} decision as outputs; optionally fails the step when denied.

- uses: https://forge.blackleafdigital.com/Public/actions/zregistry-check@main
  with:
    license: ${{ secrets.CUSTOMER_LICENSE }}     # required
    ecosystem: npm                               # required
    package: '@ztana/core'                       # required
    zbilling-url: https://billing.blackleafdigital.com    # required
    zbilling-token: ${{ secrets.ZBILLING_ENTITLEMENTS_TOKEN }}   # required
    fail-on-denied: 'false'                      # optional, default: false
    forgejo-token: ${{ secrets.FORGEJO_TOKEN }}  # to fetch the private CLI asset
# outputs: decision (JSON), allowed (true|false), reason