Workflows
Generate GitHub Actions workflow files under .github/workflows/ from the "workflows" array in nova.config.json.
The generator validates each entry against its template, builds the workflow from your config, resolves variable names, and writes the YAML to disk.
Why Use This Command?
- Bootstraps a full set of CI/CD workflows without writing YAML by hand.
- Publishes one project to multiple destinations (npm, GitHub Packages, Docker, hosting) from a single build, instead of each destination running its own pipeline from scratch.
- Serializes dependent publishes (e.g., a preset waits for its core dependency to reach the registry) via
afteron deploy destinations. - Keeps workflow definitions consistent across Nova-managed repositories.
Requirements
- Node.js runtime — Use any Node.js LTS release.
- Project root — Run the command from the directory containing the top-level
package.json. - Corepack package manager — A project with a
publishworkflow must declare an exact rootpackageManagervalue for npm, pnpm, or Yarn. Ranges, partial versions, missing values, and unsupported clients are rejected before any workflow file changes. - Configured workflows — The
"workflows"array must exist innova.config.json. Usenova utility initializeto configure it interactively.
Usage
Options
| Flag | Description |
|---|---|
-d, --dry-run | Run without writing any files. |
-r, --replace-file | Overwrite the existing file instead of creating a .nova-backup copy. |
Package Manager Selection
The project root's packageManager field selects how generated publish workflows install dependencies and run project-owned tools. Nova passes the client through the entire workflow instead of translating every project to npm.
| Root descriptor | Install step | Local executable | Workspace scripts without Turbo |
|---|---|---|---|
npm@<exact-version> | npm install | npx <tool> | npm run <script> -w <workspace> |
pnpm@<exact-version> | pnpm install | pnpm exec <tool> | pnpm --filter=<workspace> run <script> |
yarn@<exact-version> | yarn install | yarn exec <tool> | yarn workspace <workspace> run <script> |
Each install job first runs corepack enable <client>. Corepack then reads the full descriptor from the checked-out root package.json, so the workflow uses the pinned version instead of installing a floating latest release. Turbo projects use the same client to execute turbo.
This selection governs project tooling; it does not rename deploy destinations. The npm and github-packages targets still use the npm publishing protocol and commands even when pnpm or Yarn manages the repository.
Renamed fields are a breaking change
Configs written for the previous generator must migrate to the new field names (name, build, deploy, with, and the per-destination to, path, after). An old-shape field is rejected with a diagnostic naming its exact replacement, and the workflow is skipped until you update it.
Config-Driven Workflow Entries
Each entry in the "workflows" array maps to one output file. An entry contains:
| Field | Required | Description |
|---|---|---|
template | Yes | The name of a bundled workflow template (see Available Templates). |
name | Yes | A label appended to the output filename and workflow name. Must be unique within the same template. |
triggers | Yes | An array of triggers (see Available Triggers). Each trigger must be valid for the template. |
build | No | An array of workspace paths (keys from workspaces) to include in the check / build step filter. |
deploy | No | An array of publish destinations for the publish template. See Deploy Fields. |
with | No | Key-value pairs that remap secret / variable names or supply values for literal template variables. |
Deploy Fields
Each entry in the deploy array pins one publish destination to a workspace.
| Field | Required | Description |
|---|---|---|
to | Yes | One of the nine target types (see Available Target Types). |
path | Yes | Path to the workspace that owns the destination (e.g., "./packages/nova"). |
after | No | Paths of other same-type destinations that must finish publishing before this one starts. |
with | No | Key-value pairs for this destination that remap names or supply literal values, overriding the workflow-level with. |
Config Examples
Publish a single package to npm:
{
"template": "publish",
"name": "project",
"triggers": ["release"],
"build": ["./packages/core"],
"deploy": [
{
"to": "npm",
"path": "./packages/core"
}
]
}
Serialize dependent publishes with after:
{
"template": "publish",
"name": "project",
"triggers": ["release"],
"build": [
"./packages/core",
"./packages/preset"
],
"deploy": [
{
"to": "npm",
"path": "./packages/core"
},
{
"to": "npm",
"path": "./packages/preset",
"after": ["./packages/core"]
}
]
}
The preset destination waits for core to finish publishing before it starts, so consumers that pull [email protected] can always resolve its declared dependency on [email protected].
Publish to multiple destinations from one build:
{
"template": "publish",
"name": "project",
"triggers": ["release"],
"build": [
"./packages/sdk",
"./apps/docs"
],
"deploy": [
{
"to": "npm",
"path": "./packages/sdk"
},
{
"to": "github-packages",
"path": "./packages/sdk"
},
{
"to": "cloudflare-pages-docusaurus",
"path": "./apps/docs"
}
]
}
Each destination becomes its own publish job. Jobs run in parallel off a shared build job, except where after declares an explicit ordering.
Available Templates
The generator bundles three templates, each with its own trigger set.
| Template | Description | Notes |
|---|---|---|
publish | Build once and publish to one or more destinations | Uses deploy. Each destination declares its own secret / variable setup. |
lock-inactive-issues | Lock stale issues | No manual setup needed (uses automatic GITHUB_TOKEN). Pick a cadence via triggers. |
check-sponsor-gated-issues | Gate issues by GitHub sponsor status | Requires a PERSONAL_ACCESS_TOKEN secret plus several var-format with entries (see check-sponsor-gated-issues Variables). |
check-sponsor-gated-issues Variables
This template declares the following variables. GITHUB_TOKEN is supplied automatically, so the only secret you create manually is PERSONAL_ACCESS_TOKEN.
The remaining values are var-format, not literal - they resolve to ${{ vars.NAME }} references and are configured as GitHub repository variables.
| Variable | Format | Description |
|---|---|---|
GITHUB_TOKEN | secret | Automatic token supplied by GitHub Actions (no manual setup). |
PERSONAL_ACCESS_TOKEN | secret | Personal access token with read:org and read:user scopes for sponsor lookups. |
ISSUE_LABELS | var | Comma-separated labels to apply when gating issues. |
ISSUE_LIMIT_COMMENTER | var | Whether to limit comments to the issue opener and repo owners. |
ISSUE_LOCK_ON_CLOSE | var | Whether to lock issues when closed. |
ISSUE_MESSAGE_NOT_SPONSOR | var | Message shown to non-sponsors when they open an issue. |
ISSUE_MESSAGE_WELCOME | var | Welcome message shown to sponsors when they open an issue. |
IS_ORGANIZATION | var | Whether the repository owner is an organization. |
SPONSOR_ACTIVE_ONLY | var | Whether to require active sponsorship status. |
SPONSOR_EXEMPT_FILE_LOCATION | var | Path to a file listing users exempt from sponsor requirements. |
SPONSOR_MINIMUM | var | Minimum monthly sponsorship amount in dollars. |
Available Target Types
The publish template supports the following to values for each entry in deploy. Each type declares its own secrets and variables.
to value | Description | Secrets / Variables |
|---|---|---|
npm | Publish a package to npm | NPM_TOKEN (secret) - npm access token for publishing. Skip creating this secret in GitHub if using OIDC trusted publishing. |
github-action | Publish a JavaScript GitHub Action via orphan tags | None to configure on GitHub. Its ACTION_ENTRY_POINT, ACTION_OUTPUT_PATH, ACTION_YML_PATH, and RELEASE_BRANCH_NAME are literal values supplied via with (see Publishing GitHub Actions). |
github-packages | Publish a package to GitHub Packages | GITHUB_TOKEN (secret, supplied automatically - no manual setup). |
docker-hub | Publish a Docker image to Docker Hub | DOCKERHUB_TOKEN (secret) - Docker Hub access token for publishing images. DOCKERHUB_USERNAME (var) - Docker Hub username or organization name. |
ghcr | Publish a Docker image to GitHub Container Registry | GITHUB_TOKEN (secret, supplied automatically - no manual setup). |
cloudflare-pages-docusaurus | Deploy a Docusaurus site to Cloudflare Pages | CLOUDFLARE_API_TOKEN (secret) - Cloudflare API token with Pages deployment permissions. CLOUDFLARE_ACCOUNT_ID (var) - Cloudflare account ID. CLOUDFLARE_PROJECT_NAME (var) - Cloudflare Pages project name. |
cloudflare-workers | Deploy a Worker with static assets to Cloudflare Workers | CLOUDFLARE_API_TOKEN (secret) - Cloudflare API token with Workers deployment permissions. CLOUDFLARE_ACCOUNT_ID (var) - Cloudflare account ID. |
github-pages-docusaurus | Deploy a Docusaurus site to GitHub Pages | GITHUB_TOKEN (secret, supplied automatically - no manual setup). |
vercel-nextjs | Deploy a Next.js app to Vercel | VERCEL_TOKEN (secret) - Vercel access token with deploy permissions. VERCEL_ORG_ID (var) - Vercel organization ID. VERCEL_PROJECT_ID (var) - Vercel project ID. |
Available Triggers
Each template accepts a specific set of triggers, and a trigger outside that set is rejected.
| Template | Triggers |
|---|---|
publish | release, tag-push, push, workflow-run-success, workflow-run-failure, workflow-run-any |
lock-inactive-issues | schedule-daily, schedule-weekly, schedule-monthly |
check-sponsor-gated-issues | issues, issue-comment |
| Trigger | Carries workflows |
|---|---|
release | No |
tag-push | No |
push | No |
schedule-daily | No |
schedule-weekly | No |
schedule-monthly | No |
issues | No |
issue-comment | No |
workflow-run-success | Yes |
workflow-run-failure | Yes |
workflow-run-any | Yes |
The publish and lock-inactive-issues templates also include workflow_dispatch (manual trigger with dry-run option) automatically.
Configuring Publish Triggers
The publish template accepts three release-style triggers, each with its own config form.
release— Publishes when a GitHub Release is published. Configure it with the bare string"release".tag-push— Publishes when a version tag is pushed, emittingon: push: tags: ["v*"]. Configure it with the bare string"tag-push"(tags default tov*), or an object{ "name": "tag-push", "tags": ["v*"] }to override the tag globs.push— Branch continuous deployment that publishes for real on a push to a listed branch. Configure it only in object form with a requiredbranchesfilter, for example{ "name": "push", "branches": ["main"], "paths": ["packages/**"] }.
Publishing on a version tag keeps GITHUB_REF pointing at the tag, so npm --provenance signing stays valid. A release-triggered run can instead bind to the default branch and fail provenance, which is why tag-push is preferred for signed npm publishes.
The bare string "push" is a validation error, since a branchless push trigger would fire on every branch. The branches, paths, and tags filters must each be a non-empty array of strings when present.
Configuring Workflow-Run Triggers
The three workflow-run-* triggers start a new publish workflow after a named upstream workflow finishes. Choose the variant that matches the upstream result you accept:
| Trigger | When the downstream jobs run |
|---|---|
workflow-run-success | After an upstream run finishes successfully. |
workflow-run-failure | After an upstream run finishes with a failure. |
workflow-run-any | After an upstream run finishes, regardless of its conclusion. |
Declare one of these triggers in object form. Its workflows array contains sibling <template>-<name> keys from the same workflows config:
{
"template": "publish",
"name": "preset",
"triggers": [
{
"name": "workflow-run-success",
"workflows": ["publish-core"]
}
],
"build": ["./packages/preset"],
"deploy": [
{
"to": "npm",
"path": "./packages/preset"
}
]
}
Nova turns publish-core into the generated upstream workflow's display name. When that workflow finishes successfully, GitHub starts publish-preset as a separate run.
Multiple names mean “any,” not “all”
GitHub treats multiple entries in workflows as alternatives. This configuration starts publish-preset when either publish-core or publish-cli finishes successfully; it does not wait for both. If both finish, each completion can start its own downstream run.
{
"name": "workflow-run-success",
"workflows": ["publish-core", "publish-cli"]
}
If several packages must publish in one coordinated run, keep them in one publish workflow and use deploy[].after for destination ordering.
Each referenced key must resolve to a sibling workflow, and Nova rejects circular references. The downstream run starts fresh; Nova does not automatically carry build files or artifacts over from the upstream run.
GitHub also requires the downstream workflow file to exist on the repository's default branch and limits workflow_run chains to three levels. See GitHub's workflow_run documentation before designing a longer chain.
Environment Variables and Secrets
A publish workflow reads workspace environment values declared under environment.workspaces. Every value has a required reach, which tells Nova where the value is allowed to travel:
reach | Local .env | GitHub storage | CI build environment | Deployed runtime |
|---|---|---|---|---|
local | Yes | No | No | No |
managed | Yes, blank | Yes | No | No |
build | Yes | Yes | Yes | No |
runtime | Yes | Yes | No | Yes |
For managed, build, and runtime, the independent secret flag controls storage: secret: false uses a GitHub Variable, while secret: true uses a GitHub Secret. A local value never leaves the developer's machine, so it does not accept secret and does not need a namespace prefix.
A build value exists only while the build runs on the CI runner. A Variable that application code inlines (for example, import.meta.env.PUBLIC_SITE_KEY) can become part of the output; a Secret used for authentication can be consumed without being emitted. "Build" means "available to the build process," not automatically "shipped to users." Whether a value is exposed in client output depends on how the application uses it.
A runtime value is reconciled into a running server's environment and read by reference (env.X or process.env.X). A managed value is tracked locally and on GitHub but is intentionally delivered to neither destination.
Every GitHub name is the workspace's prefix joined to the key (e.g. CBN_ joined to PUBLIC_SITE_KEY becomes CBN_PUBLIC_SITE_KEY).
Build Delivery
Every workspace value with reach: "build" is written into the build .env before check / build. Nova reads it from its GitHub Variable (${{ vars.<name> }}) when secret: false, or its GitHub Secret (${{ secrets.<name> }}) when secret: true.
A secret: false Variable falls back to its defaultValue when the Variable is unset. Deploy credentials and workflow-config keys never reach the build.
The Setup report lists every GitHub Variable and Secret to create.
Runtime Environment Sync
On each deploy, Nova reconciles every workspace value with reach: "runtime" into the runtime store of every server-bearing target in the workspace's deploy[]. secret picks the channel: a secret: true value is stored encrypted, while a secret: false value is stored as a plaintext runtime variable.
- Upsert — Every declared runtime value is pushed to the destination under its prefixed name, and the running app reads it as
env.NAMEon Workers orprocess.env.NAMEon Vercel. - Delete — Any value already in the destination store but no longer declared in the config is removed, since Nova is the single source of truth for that target's runtime env.
- Log by name — Each add and removal is written to the deploy log by name only, never by value, so the reconcile stays auditable.
Sync makes Nova authoritative
Nova has full authority over each server's runtime env — removing a reach: "runtime" value from the config deletes it from every server on the next deploy, and any value set out-of-band is reconciled away. The sync runs unattended in CI, so there is no interactive confirm — the deploy log records every change.
Per-Target Mechanism
The config is identical across both targets; only the deploy step differs.
| Target | Reconcile mechanism | Runtime scope |
|---|---|---|
cloudflare-workers | wrangler secret for encrypted values and wrangler deploy --var for plaintext, run around wrangler deploy. | Live worker. |
vercel-nextjs | vercel env add and remove for both channels, run before the prebuilt deploy. | Production only. |
Dev is unchanged — the values live in the local .env from nova generate must-haves dotenv, which you fill in with real values.
Variable Formats
Each template declares variables with one of three formats. The format determines how the value is substituted into the generated YAML:
| Format | Substitution behavior |
|---|---|
secret | Produces ${{ secrets.NAME }} in the output. If a with entry is provided, the secret reference uses that value as the name instead. |
var | Produces ${{ vars.NAME }} in the output. If a with entry is provided, the variable reference uses that value as the name instead. |
literal | Replaces the placeholder with the exact string from with. A literal variable without a default must be provided or the entry is skipped. |
Output Filename Convention
Generated files are written to .github/workflows/ with the naming pattern:
nova-{template}-{name}.yml
Examples:
nova-lock-inactive-issues-project.ymlnova-publish-project.ymlnova-check-sponsor-gated-issues-project.yml
How It Works
Reading Config
The generator loads nova.config.json and reads the "workflows" array. If the array is missing or empty, the generator exits with no changes.
Validation
When at least one valid publish entry exists, the generator validates the root packageManager first. If that project-level setting is missing or invalid, the whole generation run stops before writing, backing up, or deleting workflow files.
The generator also checks each configured entry:
- No entry carries an old-shape field name — a renamed field is rejected with a hint naming its replacement (e.g.,
Workflow field "suffix" was renamed to "name". Skipping.). - The template name matches a known bundled template.
nameis present and a non-empty string, and no two entries share the same template andname.- Each trigger is valid for the entry's template, and object-form filter lists are non-empty string arrays.
- A
pushtrigger uses the object form with abranchesfilter, and a workflow-run trigger carriesworkflowsreferences that each resolve to a sibling workflow. - Each
buildscope is a registered workspace, and no workspace still carries the retireddotenvblock — that model moved to the top-levelenvironmentblock classified byreachand, where applicable,secret, so an old-shape config is rejected with a migration hint. - Each deploy
tois a supported target type, and each deploypathis a registered workspace. - All
literalvariables without a default have a corresponding value inwith. - Destination uniqueness holds across every workflow (see Cross-Workflow Uniqueness), and no two entries produce the same output filename.
Each entry-level failure emits a diagnostic and skips the affected workflow, while other valid workflows still build. The project-level package manager guard is the exception because every publish entry shares it.
File Generation
Each valid entry produces one YAML file under .github/workflows/. The generator builds the workflow blueprint from your config — merging the resolved triggers, jobs, deploy destinations, and selected Corepack client — resolves variable placeholders from the template metadata and the entry's with, then serializes the blueprint to YAML.
Deploy jobs that declare after dependencies emit needs: ["build", ...]. If the target file already exists, a .nova-backup copy is created unless --replace-file is set.
Orphan Cleanup
Orphan cleanup runs only when the config validated with zero diagnostics. A config with any error never deletes your existing workflow files, so a rejected workflow's file is left untouched.
When the config is clean, the generator scans .github/workflows/ for existing nova-*.yml files that are not in the current config. These orphaned files are backed up (renamed with a .nova-backup timestamp) or deleted when --replace-file is set.
Setup Instructions
After all files are generated, the generator prints a Setup report of the secrets and variables to configure on GitHub. This covers every non-automatic template secret and var, plus every reach: "build" workspace value each build scope reads — listed under its prefixed GitHub name as a Variable (secret: false) or a Secret (secret: true).
Entries are deduplicated and listed once per output file, so a value used by two destinations in the same workflow appears a single time.
Publishing GitHub Actions
The github-action destination distributes a JavaScript GitHub Action by maintaining an orphan releases branch whose commits contain only the runtime files needed by consumers. Each release is tagged with its semver (e.g., v2.1.0), and the floating major tag (v2) is force-moved to the latest release commit.
Older frozen major tags (e.g., v1) are never touched.
Expected Repo Shape
action.ymlat the repo root (top-level, not inside a workspace).packages/<action-name>/— action source (role:app, policy:trackable).apps/docs/— Docusaurus docs (role:docs).- Build output in the workspace's
build/directory, following the standard Nova convention.
Release Tree Contents
What lands on the releases branch for each release:
Required (publish fails if missing):
action.ymlaction/— the built output (renamed from the workspace'sbuild/directory during publish).
Optional (copied if present at repo root, skipped silently if not):
README.mdLICENSESECURITY.mdNOTICECHANGELOG.md
Example nova.config.json Entry
{
"workflows": [{
"template": "publish",
"name": "release",
"triggers": ["release"],
"deploy": [
{
"to": "github-action",
"path": "./packages/my-action",
"with": {
"ACTION_ENTRY_POINT": "index.js",
"ACTION_OUTPUT_PATH": "./packages/my-action/build",
"ACTION_YML_PATH": "./action.yml",
"RELEASE_BRANCH_NAME": "releases"
}
}
]
}]
}
All four with values are required. Defaults aren't auto-applied — nova utility initialize prompts for each one, showing the suggested value as an example.
You can set these on the deploy's with as shown, or on the workflow-level with to share them across the entry. A deploy-level with value takes precedence over the workflow-level one for that destination.
| Value | Description |
|---|---|
ACTION_ENTRY_POINT | Filename of the bundled action entry script (relative to ACTION_OUTPUT_PATH). Example: index.js. |
ACTION_OUTPUT_PATH | Path to the built action output directory. |
ACTION_YML_PATH | Path to the action.yml file from repo root. |
RELEASE_BRANCH_NAME | Branch name for the orphan release history. |
Release Mechanics
When a GitHub Release is published:
- The base
buildjob runs the workspace's build command through the rootpackageManagerclient (executing Turbo when aturbo.jsonis present, otherwise using the client's workspace command), producing output in the workspace'sbuild/directory. - The
publish-github-action-*job spins up a separate git workspace at$RUNNER_TEMP/release, initialized with the release branch name. - If the release branch already exists remotely, the workspace fetches and resets to it. Otherwise it stays as a fresh orphan branch.
- The release tree is staged:
action.yml, the optional files above, and the built output copied intoaction/(renamingbuild/→action/). - The staged tree is committed as
Release <tag>and force-pushed to the release branch. - The semver tag (e.g.,
v2.1.0) is force-retagged onto the new orphan commit. GitHub creates this tag onmain's HEAD when you publish the release; this step moves it to point at the orphan commit souses: owner/[email protected]resolves correctly. - The floating major tag (e.g.,
v2) is force-moved to the same commit. - A SLSA build-provenance attestation is emitted on the resolved entry script (
ACTION_OUTPUT_PATH/ACTION_ENTRY_POINT) viaactions/attest-build-provenance.
The main branch is never rewritten — the release branch is a parallel, orphan history that mirrors only what consumers receive via uses: owner/repo@<ref>.
Implications of the orphan-branch design
A few non-obvious consequences worth knowing:
mainand the release branch share no history. Runninggit log mainwill never show release commits. Runninggit log <release-branch>shows only release commits, never source commits. To find whichmaincommit produced a given release, usegit logwith the release's authoring date or look at the release's GitHub UI metadata.- Checking out a tag yields the runtime tree, not the source.
git checkout v2.1.0puts you in a working tree containing onlyaction.yml,action/index.js, and the optional community files. The original source that produced this release is at the parent main commit at the time of release publication. - Force-retagging breaks reproducibility from a tag. Once
v2.1.0is moved frommain's HEAD to the orphan commit, the originalmaincommit that triggered the release is no longer reachable via that tag. Keep release notes that reference the original commit SHA if you need that traceability. - Older frozen major tags stay put. A
v1tag from a prior major line is never touched by this workflow. Only the floating major matching the releasedv<N>.x.y(e.g.,v2for av2.1.0release) gets force-moved.
Multiple actions in one repo
If a repo ships more than one GitHub Action (rare), each github-action destination must declare a distinct RELEASE_BRANCH_NAME across the entire nova.config.json. Two github-action destinations sharing the same release branch will race on push — both jobs run in parallel and force-push to the same branch, so the second writer overwrites the first.
Nova validates destination uniqueness across the entire workflows[] array, not just within a single workflow: declaring two github-action destinations anywhere in your config — same workflow or separate workflows — with overlapping RELEASE_BRANCH_NAME values fails the generator with a clear error message and rejects both colliding workflows. To ship two actions from one repo, use distinct RELEASE_BRANCH_NAME values (e.g., releases-primary, releases-secondary) — typically each in its own workflow with its own name.
Example layout:
{
"workflows": [
{
"template": "publish",
"name": "primary",
"triggers": ["release"],
"deploy": [
{
"to": "github-action",
"path": "./packages/primary-action",
"with": {
"ACTION_ENTRY_POINT": "index.js",
"ACTION_OUTPUT_PATH": "./packages/primary-action/build",
"ACTION_YML_PATH": "./action.yml",
"RELEASE_BRANCH_NAME": "releases-primary"
}
}
]
},
{
"template": "publish",
"name": "secondary",
"triggers": ["release"],
"deploy": [
{
"to": "github-action",
"path": "./packages/secondary-action",
"with": {
"ACTION_ENTRY_POINT": "index.js",
"ACTION_OUTPUT_PATH": "./packages/secondary-action/build",
"ACTION_YML_PATH": "./packages/secondary-action/action.yml",
"RELEASE_BRANCH_NAME": "releases-secondary"
}
}
]
}
]
}
Each action gets its own publish workflow with a distinct RELEASE_BRANCH_NAME, and the GitHub Releases for each action use a uniquely-prefixed tag (e.g., primary-v1.2.3, secondary-v1.0.0). Nova's force-retag step is prefix-aware: the floating major derivation matches an optional alphanumeric prefix before the v (so primary-v1.2.3 → primary-v1, v2.1.3 → v2).
Consumers reference each action via its prefixed tag: uses: owner/repo@primary-v1, uses: owner/repo@secondary-v1.
Similarly, only one github-pages-docusaurus destination may be declared across all workflows in your nova.config.json — GitHub Pages is repo-scoped, so two deployments would overwrite each other regardless of whether they live in the same workflow or separate ones. Nova validates this at workflow-generation time.
The same cross-workflow uniqueness check applies to these other publish targets:
| Target | Uniqueness check |
|---|---|
cloudflare-pages-docusaurus | CLOUDFLARE_PROJECT_NAME must not be duplicated across workflows. |
vercel-nextjs | VERCEL_ORG_ID + VERCEL_PROJECT_ID must not be duplicated across workflows. |
github-action | RELEASE_BRANCH_NAME must not be duplicated across workflows. |
github-pages-docusaurus | At most one destination across all workflows (GitHub Pages is repo-scoped). |
Tag protection rules
The force-retag steps require permission to overwrite existing tags. If the consuming repo has tag protection rules covering the v* pattern, the workflow's default GITHUB_TOKEN will be denied — even with contents: write permission.
Either disable tag protection for the patterns you ship, or replace GITHUB_TOKEN with a GitHub App token that has the required maintain-permission scope.