Landmark is a portable release-intelligence runtime for repositories that use conventional commits. It can run as a GitHub Action, but the product boundary is the Rust CLI: local scripts, generic CI systems, and agents can invoke the same runtime to produce version decisions, technical changelogs, public release notes, release-kit plans, feeds, and machine-readable evidence.
- Uses a checked-in Rust runtime for Landmark-owned release behavior
- Sets up Node.js only when full semantic-release mode is requested
- Installs
semantic-releaseand release plugins - Runs
semantic-release(version bump, changelog update, release creation) - Optionally synthesizes user-facing notes from technical changelog content
- Updates the GitHub Release body to prepend a
## What's Newsection - Plans final-mile artifacts such as docs updates, blog drafts, demo scripts, images, GIFs, and videos through typed producer contracts
- Optionally creates a GitHub issue when synthesis/update fails and exposes synthesis status output
Landmark's product boundary is the Rust CLI. Start locally, then choose the smallest integration mode that matches the release system you already have.
Use this first in a checkout. It requires no secrets and does not call GitHub or an LLM:
cargo run --locked -p landmark -- run --provider local --repo-root .The command reads git tags and conventional commits, chooses the next semantic
version when --release-tag is omitted, writes .landmark/run/evidence.json,
generates a technical changelog, writes .landmark/run/release-kit.json, and
writes markdown, plaintext, HTML, JSON, and RSS release artifacts under
docs/releases/. The evidence packet also embeds the release-kit plan so
--dry-run can print the complete final-mile artifact graph without writing
files.
The JSON artifact is the zero-runtime marketing-site export. Each
docs/releases/releases.json entry declares
schema_version: landmark.public-release-notes.v1 plus repository,
release_url, audience, markdown, html, plaintext, and parsed
sections, so a static site can render the latest public notes without loading
Landmark or re-reading git history.
Build from source with cargo run --locked -p landmark -- ... or a locally built
target/debug/landmark, or download a published per-target release binary
(landmark-x86_64-unknown-linux-musl, landmark-aarch64-unknown-linux-musl,
landmark-aarch64-apple-darwin, landmark-x86_64-apple-darwin) plus
checksums.txt from a GitHub Release.
The GitHub Action downloads and checksum-verifies the matching binary itself;
it no longer ships a checked-in binary.
The executable quickstart oracle is:
cargo run --locked -p landmark -- replay-action --scenario first_run_local_previewrelease-transaction prepare computes the version, tag, notes digest, source
revision, and deterministic transaction identity without creating a public tag
or release. A product build can then supply an OCI digest, portable release
manifest, and Sigstore bundle to release-transaction bind. Binding is local,
locked, compare-and-swap idempotent, and rejects substitution. This slice uses
one concrete portable transport: three regular files under a caller-selected
local artifact root. Landmark walks that root with directory descriptors so an
ancestor swap cannot escape confinement, rejects absolute paths, parent
traversal, and symlinks, and recomputes each file digest. The signed publication
manifest must reproduce the exact prepared candidate and bind the exact OCI
descriptor digest and media type. The signature bundle must be a Sigstore v0.3
bundle that passes cosign verify-blob before the canonical transaction changes
from prepared to ready. A ready retry must request the exact stored key hash
or keyless identity and issuer; Landmark rejects trust-policy substitution. This
slice does not yet prove a remote registry or publish/reconcile GitHub state.
landmark release-transaction prepare \
--repo-root . --repository owner/product \
--transaction .landmark/release-transaction.json
landmark release-transaction bind \
--transaction .landmark/release-transaction.json \
--artifact-manifest .landmark/release-artifacts.json \
--artifact-root .landmark/release-artifacts \
--verification-key /trusted/landmark-release-signing.pubThe packet schemas are schemas/release-transaction.v1.schema.json,
schemas/release-artifact-manifest.v1.schema.json, and
schemas/release-publication-manifest.v1.schema.json. The artifact manifest names
normalized paths relative to --artifact-root plus expected SHA-256 digests.
The OCI descriptor is bound only by its recomputed local digest; a published
registry reference is deliberately absent until the future public commit
phase proves it exists remotely. For keyless Sigstore verification, replace
--verification-key with both
--certificate-identity and --certificate-oidc-issuer. Trusted-key mode
verifies the bundle signature offline against that key; keyless mode keeps
Sigstore transparency-log verification enabled.
release-transaction commit applies the public mutations and completes the
release transaction (ADR 0004). It inspects before it writes: the tag ref and
release record are resolved from the GitHub API, tag identity is authoritative
(a moved tag fails closed), drafts block adoption, and a release already
consistent with the immutable candidate is adopted without mutation. On
success the transaction becomes completed and carries a receipt binding the
source revision, tag, release id, and release URL. Retries re-verify that the
recorded release still backs the receipt and then return the same completed
result; drift or deletion is a visible failure, never a silent success.
landmark release-transaction commit \
--transaction .landmark/release-transaction.json \
--notes-file .landmark/whats-new.md
# Inspect without mutating anything:
landmark release-transaction commit \
--transaction .landmark/release-transaction.json --dry-runGITHUB_TOKEN or GH_TOKEN supplies the credential; pass --github-token
explicitly elsewhere. --repository must match the transaction's immutable
candidate — mismatches fail closed; re-prepare instead of overriding.
Scope note: Landmark's own self-release pipeline shares this reconciliation core but does not yet bind OCI/Sigstore artifact identities, so it publishes without emitting a completed receipt.
A shell script, GitLab CI job, Forgejo workflow, Buildkite step, or agent can run the same Rust runtime directly:
cargo run --locked -p landmark -- run \
--provider local \
--repo-root . \
--output-dir .landmark/run \
--output-file docs/releases/{version}.md \
--output-json docs/releases/releases.json \
--rss-feed-file docs/releases/feed.xmlUse --dry-run when you only want the evidence preview on stdout. Use
--provider github --publish-release-body only when the CI job is explicitly
allowed to mutate an existing GitHub Release.
Use full mode when Landmark should run semantic-release, create the release,
then synthesize and publish notes:
name: Release
on:
workflow_run:
workflows:
- CI
types:
- completed
branches:
- main
- master
workflow_dispatch:
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
# Short-lived installation token from a GitHub App installed on this
# repo, in place of a personal access token. See "Why a GitHub App,
# not a PAT" below.
- name: Mint release token
id: release-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.LANDMARK_RELEASER_APP_ID }}
private-key: ${{ secrets.LANDMARK_RELEASER_PRIVATE_KEY }}
# Landmark: Automated semantic-release pipeline
# https://github.com/misty-step/landmark
- name: Run Landmark
uses: misty-step/landmark@v0
with:
github-token: ${{ steps.release-token.outputs.token }}
llm-api-key: ${{ secrets.OPENROUTER_API_KEY }}
# Optional: customize model and fallbacks
# llm-model: deepseek/deepseek-v4-flash-0731
# llm-fallback-models: "google/gemini-3.7-flash,deepseek/deepseek-v4-pro-0813"Landmark is language-agnostic. Your repo does not need package.json or Node.js
unless full mode is running semantic-release; the action handles its own Node
24 runtime setup.
github-token accepts any token with repo write access, but a personal access
token ties the release pipeline to one person's account, expires or gets
revoked out of band, and carries whatever scope that account has everywhere
else. Prefer a GitHub App:
- Short-lived:
actions/create-github-app-tokenmints a token that lives for one run, not months. - Scoped to exactly what Landmark needs: install the App only on the repos it releases, with Contents (read/write) and nothing else unless a plugin requires it.
- Tags still trigger downstream workflows. This is the part
secrets.GITHUB_TOKEN(the ambient per-run token GitHub Actions already gives every job, no setup required) cannot do: tags and releases it creates do not firepush: tagsorrelease: publishedon other workflows in the same repo. An installation token from an App does. If your pipeline has nothing downstream of the release tag,GITHUB_TOKENis simpler and requires no secrets at all — reach for the App only when you need that trigger.
Create the App once per organization (Settings → Developer settings →
GitHub Apps; Contents: Read & write, Metadata: Read-only), install it on
your release repos, and store its App ID and private key as
LANDMARK_RELEASER_APP_ID / LANDMARK_RELEASER_PRIVATE_KEY (or repo-local
equivalents) — every example in this README uses those names.
Full mode's version/changelog/tag/release decisions are semantic-release's,
kept only as a named compatibility path for repos that already run it.
Landmark's own Rust version-decision engine (landmark run,
prepare-self-release) is the source of truth everywhere else; semantic-release
stays wired into full mode until a Rust-owned full-mode v2 can analyze, version,
changelog, tag, and publish without it.
Landmark supports two versioning modes, selected by the stability input (and
applied identically by both version engines — the bundled semantic-release
config and the Rust engine):
- Stable (
>= 1.0.0) — standard SemVer. Breaking changes bump major, features bump minor, fixes/perf bump patch. - Pre-stable (
0.x) — Cargo-compatible 0.x rules. On a0.xline the minor position is the breaking boundary, so bumps are demoted one step: a breaking change bumps minor (0.16.0→0.17.0), a feature or fix bumps patch (0.15.1→0.15.2). A pre-stable repo never auto-crosses into1.0.0— crossing to stable is an explicit, human act.
stability: auto (the default) detects the mode from the highest semver-formatted
tag on the remote (read via git ls-remote, so a stale or failed local fetch
can never mis-detect the line): below 1.0.0, or no tags at all, resolves to
pre-stable; 1.0.0 and above resolves to stable. Exotic or unrecognized tag
formats (e.g. a remote whose only version tags are prereleases) resolve to stable.
If the remote tags cannot be read at all, the run fails loudly and is
retryable rather than versioning blind — a transient failure must not route a
0.x line to stable rules. Set stability: pre-stable or stability: stable to
force a mode; forcing pre-stable on a line already at >= 1.0.0 fails fast
before publishing, while stability: stable may proceed without remote
verification (you have declared the line). A repo that ships its own
semantic-release config keeps full control and this policy does not apply.
For a brand-new pre-stable repo with no tags, the action seeds a v0.0.0 tag on
the first commit so the first release computes below 1.0.0 (e.g. a first
feature releases v0.1.0) instead of jumping to 1.0.0. Seeding requires a full
clone — check out with fetch-depth: 0 (actions/checkout defaults to a shallow
clone, whose first-commit lookup returns the graft boundary and would seed the
wrong commit); on a shallow checkout seeding is skipped with a warning.
Floating major tags follow the same 0.x semantics: a v0 tag aggregates every
0.x release, and because 0.x minor bumps are breaking, pinning @v0 accepts
breaking updates. Pin an exact tag (e.g. @v0.4.0) when you need stability.
Promotion is a deliberate manual step. When the project is ready for its 1.0.0
commitment, push the tag:
git tag v1.0.0
git push origin v1.0.0With stability: auto, the next run detects the >= 1.0.0 tag and switches to
stable SemVer automatically. No config change is required.
Use synthesis-only when release-please, Changesets, manual GitHub Releases, or a custom pipeline already creates the version and release. Mint a token first (see Why a GitHub App, not a PAT), then run Landmark after your release-creating step:
- uses: misty-step/landmark@v0
with:
mode: synthesis-only
release-tag: ${{ steps.release.outputs.tag_name }}
github-token: ${{ steps.release-token.outputs.token }}
llm-api-key: ${{ secrets.OPENROUTER_API_KEY }}This skips semantic-release entirely. Ready-to-use synthesis-only examples:
| Tool | Example | Trigger |
|---|---|---|
| release-please | examples/release-please.yml |
Push to main; release-please creates the release |
| Changesets | examples/changesets.yml |
Push to main; Changesets publishes packages |
| Changesets monorepo | examples/changesets-monorepo.yml |
Push to main; matrix per published package |
| Manual GitHub Releases | examples/manual-tag.yml |
release.published event |
Agents should start with the self-description document instead of scraping this README:
landmark describe --jsonThe describe payload exposes commands, modes, providers, inputs, outputs,
schemas, examples, preview policy for mutating commands, and the failure
taxonomy. stdout carries JSON payloads; stderr carries logs and errors. Use
--error-format json on any command to receive a stable failure envelope with
code, stage, retryable, user_action, and redacted context.
The cold-agent integration oracle is:
landmark replay-action --scenario agent_native_contracts --format jsonThe checked schema registry lives in schemas/:
schemas/landmark-manifest.v1.schema.jsonfor.landmark.ymlschemas/synthesis-status.v1.schema.jsonfor synthesis status outputschemas/release-context.v1.schema.jsonfor deterministic release context packetsschemas/release-kit.v1.schema.jsonfor final-mile release kit plans and producer contractsschemas/replay-result.v1.schema.jsonfor replay evidenceschemas/fleet-plan.v1.schema.jsonfor fleet adoption plansschemas/release-entry.v1.schema.jsonfor release-note JSON entriesschemas/run-evidence.v1.schema.jsonforlandmark runevidence packetsschemas/failure-envelope.v1.schema.jsonfor--error-format jsonstderrschemas/release-transaction.v1.schema.jsonfor prepared and artifact-bound transactionsschemas/release-artifact-manifest.v1.schema.jsonfor product-supplied immutable artifact setsschemas/release-publication-manifest.v1.schema.jsonfor the signed candidate-to-OCI binding
For the full cold-agent contract, see docs/agent-integration.md.
Landmark's durable role is release truth and artifact coordination, not every creative production engine. It should own the typed release kit:
- release facts, classification, and audience-specific importance
- artifact recommendations, paths, dependencies, and acceptance checks
- provenance for the sources each artifact used
- approval state, blockers, and waivers
- producer contracts for richer outputs such as docs patches, migration guides, blog posts, essays, social copy, screenshots, images, GIFs, and demo videos
Landmark should directly produce simple/default text and data artifacts close to release truth: technical changelogs, public notes, markdown/plaintext/HTML/JSON entries, RSS feeds, migration notes, documentation patch suggestions, and announcement/blog drafts. Specialized media and publishing work belongs behind explicit producer adapters: local CLIs, browser capture, external services, harness skills, or human approval. A video producer should receive a release-kit brief and return artifact paths, hashes, evidence, and review status; it should not become part of the core release engine.
This keeps the core deep: Landmark decides what the release needs, why it needs it, what facts must be preserved, and whether the final packet is complete. The repo or organization chooses the producers that satisfy those contracts.
Before wiring a release workflow, run Landmark's setup analyzer from a checkout:
landmark init --repo-root . --output .landmark.yml --dry-run
landmark setup --repo-root . --output-dir .landmark/setupinit infers a first .landmark.yml from package metadata, README content,
release-tool signals, and the repository name. setup then inspects
release-tool signals, default branch, tag format, required secrets,
permissions, package topology, recent conventional-commit usage, and any
checked-in .landmark.yml. It prints a JSON report with a recommended Landmark
mode and writes workflow candidates for semantic-release, release-please,
changesets, changesets monorepos, and manual-tag repositories. Every generated
workflow includes healthcheck: 'true', the default GITHUB_TOKEN (via
github-token: ${{ steps.release-token.outputs.token }}), OPENROUTER_API_KEY, and the
contents, issues, and pull-requests permissions Landmark needs.
Landmark can plan adoption across many GitHub repositories before opening any branches:
landmark fleet scan \
--owner phrazzld \
--owner misty-step \
--output .landmark/fleet.json
landmark fleet plan \
--input .landmark/fleet.json \
--output-dir .landmark/fleet-plan
landmark fleet open-prs \
--dry-run \
--plan-dir .landmark/fleet-plan \
--output-dir .landmark/fleet-plan/prsfleet scan is read-only. It lists repository activity, default branch,
archive/private state, detected release tooling, tag format, package topology,
workflow files, Landmark presence, branch-protection availability, and required
secret metadata. Secret values are never requested or printed. If GitHub hides
secret or branch-protection metadata for a repository, the scan records
unavailable with the missing scope or access boundary instead of guessing.
The default scan uses bounded concurrency and avoids expensive per-repo secret
and branch-protection probes; pass --deep-checks for a smaller owner slice
when you want GitHub to verify branch protection and Actions secret names.
fleet plan classifies each repository by kind and release surface, including
non-release repositories that should not publish release artifacts, then ranks
it into an integration mode: local, generic-ci, github-full,
github-synthesis-only, manifest-only, backfill-first, blocked, or
skipped. It writes .landmark/fleet-plan/plan.json and a Markdown operator
dashboard with repository kind, release surface, integration rationale, risk
flags, required secret names, missing secrets, skip reasons, workflow patch
paths, initial version/tag recommendations, artifact paths, rollback guidance,
historical backfill preview commands, and migration notes. GitHub secrets are
required only for GitHub integration modes; local, generic CI, manifest-only,
backfill-first, and skipped non-release plans avoid secret blockers. Active
application and library repositories with packages but no release tags or
release automation enter the backfill-first lane; documentation/config-only
repositories with no package topology remain skipped as non-release.
Repositories with existing release-please or Changesets workflows get generated
patch records for those workflow paths. Repositories with existing
semantic-release workflows are blocked until an operator explicitly chooses
whether Landmark should replace the full release job. Workflow patch records
preserve the existing workflow YAML body, add or replace jobs.synthesize, and
block instead of serializing a workflow body that contains obvious token-like
literals.
fleet open-prs --dry-run renders the exact .landmark.yml, any generated
existing-workflow updates such as .github/workflows/release-please.yml or
.github/workflows/changesets.yml, fallback
.github/workflows/landmark-release.yml for repos without an owning workflow,
diff.md, and open-prs.json receipt Landmark would propose for each eligible
repository under .landmark/fleet-plan/prs/. Local, generic CI,
backfill-first, and manifest-only modes do not get a GitHub release workflow.
For backfill-first, the PR is manifest-only/local-artifacts adoption: it adds
.landmark.yml with local artifact destinations and carries the first-release
approval notes in diff.md; it does not require GH_RELEASE_TOKEN,
OPENROUTER_API_KEY, or release-body mutation.
Confirmed rollout requires --confirm-remote --max-prs 1; the receipt records
branch names, commit messages, rollback/disposition guidance, evidence
directories, and an APPLY.md packet with the remote branch, commit, PR,
rollback, and monitoring commands. Operators merge one downstream PR, watch its
release run, then continue the fleet rollout deliberately.
For the Misty Step factory-wide adoption standard, including when to choose
full mode versus synthesis-only, see
docs/fleet-integration-playbook.md.
Landmark reads .landmark.yml from the repository root before synthesis. It
keeps product context, audience, voice, changelog source, artifact outputs, and
model policy in the repo instead of requiring every workflow to repeat them.
Non-empty action inputs still win over manifest values.
When model.primary is omitted, model.policy selects Landmark's built-in
default model tier: cheap uses deepseek/deepseek-v4-flash-0731, balanced
uses the same Flash pin, and rich escalates to deepseek/deepseek-v4-pro-0813. off disables LLM synthesis while
still publishing the technical release.
product:
name: Landmark
description: Versioning, changelog, and release-note automation.
audience: developer # general, developer, end-user, enterprise
voice: Clear, concrete, and release-operator friendly.
changelog:
source: auto # auto, changelog, release-body, prs
artifacts:
markdown: docs/releases/{version}.md
plaintext: docs/releases/{version}.txt
html: docs/releases/{version}.html
json: docs/releases/releases.json
rss: docs/releases/feed.xml
release:
profile: full # full or synthesis-only
model:
policy: balanced # cheap, balanced, rich, off
primary: deepseek/deepseek-v4-flash-0731
fallbacks:
- google/gemini-3.7-flash
- deepseek/deepseek-v4-pro-0813
budget:
max_input_tokens: 12000
max_output_tokens: 1200
max_usd: 0.25Use landmark doctor --repo-root . to validate manifest enums before a
workflow run. Use landmark setup --repo-root . --output-dir .landmark/setup after editing the manifest to regenerate workflow candidates
that reflect the durable defaults.
Use landmark synthesize --dry-run-cost ... to inspect the release context
packet without calling an LLM. The dry run reports deterministic repo facts,
estimated input/output tokens, model tier, selected model, skip/use/escalation
decision, cost estimate, deterministic release classification, and the final
context sources included in the prompt. In balanced mode, docs-only,
chore-only, dependency-only, and internal-tooling releases are skipped; breaking,
security, and migration-heavy releases escalate to the rich tier — unless an
explicit model.primary is configured, which always wins.
During real synthesis runs with model.policy enabled and an API key present,
Landmark first sends the parsed commits, commit bodies, diff statistics, and the
rendered changelog context through a cheap OpenAI-compatible classifier.
Classification and note synthesis resolve fallbacks under different rules:
- Classification uses the configured
model.primarywhen set; otherwise the pinned classification tierdeepseek/deepseek-v4-flash-0731, followed by any configured fallbacks and the classification fallbackgoogle/gemini-3.7-flash. It never inherits the runtime-derived synthesis fallback chain —deepseek/deepseek-v4-pro-0813classifies nothing unless explicitly configured. - Note synthesis attempts the selected primary first, then configured
fallbacks, or the runtime-derived chain from
crates/landmark/src/model_policy.rswhen nothing is configured. Underbalancedpolicy with a policy-derived Flash primary, high-significance releases escalate to the rich pin; an explicit primary is kept for the rich attempt instead of being replaced by the pin. These rules apply on every endpoint. Conventionalfeat,fix,perf, security, migration, and breaking-change signals remain the deterministic floor: if the model would downgrade or skip them, Landmark records the disagreement in the synthesis context, preserves a synthesis-worthy classification, and appends a short classification notice to the generated release notes.
Use landmark run --provider local --repo-root . to write a release-kit
plan at .landmark/run/release-kit.json and record its schema and hash in
.landmark/run/evidence.json; the evidence packet includes the deterministic
version decision and changed-file list so downstream producers do not have to
re-read git state. For Rust crate repositories, the version decision also runs
cargo semver-checks --baseline-rev <previous-tag> against the current checkout
and records the public API evidence as version_decision.api_evidence.
Conventional commits remain the deterministic floor: semver evidence can
upgrade the bump, a provider skip or failure is recorded without silently
changing the floor, and a commit/API disagreement records a typed waiver need
instead of hiding the conflict. --dry-run keeps the filesystem untouched and
prints the same release_kit object inside the stdout evidence packet. Low internal
releases keep the kit to Landmark-owned changelog and notes evidence. Eligible
non-patch release moments also embed gated social post drafts in the kit: two
short variants, an angle note, the release evidence link, and the configured
voice card. Those drafts are Landmark-owned text artifacts, but their approval
state stays pending for operator review and Landmark has no autopost path.
High-importance, security, breaking, or migration-heavy releases add planned
producer-adapter artifacts such as migration guides, docs updates, blog drafts,
and demo videos with explicit handoff contracts, evidence paths, and pending
approval state.
landmark notify-release-feed is the first remote-service producer adapter for
the release kit. It reads .landmark/run/evidence.json and
.landmark/run/release-kit.json, fills the text-floor artifacts
(version-decision, changed-files, and changelog-diff) as produced
producer-adapter outputs, and POSTs the full landmark.release-kit.v1 JSON body
to the receiver. The adapter uses the same signature scheme as notify-webhook:
X-Signature-256: sha256=<hmac>, computed over the raw JSON body. Configure it
with RELEASE_FEED_URL and RELEASE_FEED_SECRET; LANDMARK_RELEASE_FEED_* and
RELEASE_KIT_FEED_* are accepted aliases. Missing URL or secret config is a
clean skip.
Use landmark backfill --repo-root . --since <tag> --mode artifacts-only --dry-run to preview historical artifact migration for repositories that
already have release tags. For backfill-first repos, create the
operator-approved initial tag only after inspecting the fleet plan
recommendation, then run the preview command before enabling any
release-mutating workflow.
| Input | Required | Default | Description |
|---|---|---|---|
mode |
No | full |
Pipeline mode: full (semantic-release + synthesis) or synthesis-only (synthesize for existing tag). |
release-tag |
No* | "" |
Release tag to synthesize notes for (required when mode: synthesis-only). |
github-token |
Yes | - | GitHub App installation token or PAT with repo write access. Used by semantic-release and GitHub API update calls. See Why a GitHub App, not a PAT. |
llm-api-key |
No* | - | API key for synthesis (OpenRouter, OpenAI, or compatible providers). |
llm-model |
No | manifest policy default | Primary model ID for note synthesis. |
llm-fallback-models |
No | manifest, then runtime-derived chain (see crates/landmark/src/model_policy.rs) |
Comma-separated fallback model IDs tried in order if primary fails; with neither set, the Rust runtime derives the chain at attempt time from the pinned tiers, excluding the selected primary. |
llm-api-url |
No | https://openrouter.ai/api/v1/chat/completions |
OpenAI-compatible chat completions endpoint URL. |
node-version |
No | 24 |
Node.js version used to run semantic-release. |
stability |
No | auto |
Versioning stability policy: auto (detect from the latest tag — below 1.0.0 or untagged is pre-stable), pre-stable (Cargo-style 0.x rules), or stable (standard SemVer). Ignored when the repo ships its own semantic-release config. See Versioning Philosophy. |
synthesis |
No | true |
If true, generate and prepend user-facing notes. |
synthesis-required |
No | false |
If true, fail the action when synthesis/update fails (after failure reporting). |
synthesis-strict |
No | false |
Deprecated alias for synthesis-required. |
synthesis-failure-issue |
No | false |
If true, create a GitHub issue in the consuming repository when synthesis/update fails. |
notes-output-file |
No | manifest, then "" |
Write synthesized notes to this file path. Use {version} placeholder for the release tag (e.g., docs/releases/{version}.md). |
notes-output-text-file |
No | manifest, then "" |
Write synthesized notes as plaintext to this file path. Use {version} placeholder (e.g., docs/releases/{version}.txt). |
notes-output-html-file |
No | manifest, then "" |
Write synthesized notes as an HTML fragment to this file path. Use {version} placeholder (e.g., docs/releases/{version}.html). |
notes-output-json |
No | manifest, then "" |
Append a structured release entry to this JSON array file. Creates the file if it does not exist. |
prompt-template-path |
No | "" |
Path to a custom synthesis prompt template relative to repo root. Overrides audience and convention-based detection. |
audience |
No | manifest, then general |
Built-in prompt variant used when no custom prompt template is found. One of: general, developer, end-user, enterprise. |
product-description |
No | manifest, then "" |
One-line product description injected into the synthesis prompt as {{PRODUCT_CONTEXT}}. |
voice-guide |
No | manifest, then "" |
Tone/style guidance injected into the synthesis prompt as {{VOICE_GUIDE}}. |
changelog-source |
No | manifest, then auto |
Technical source for synthesis. auto tries CHANGELOG.md, then release body, then merged PR extraction. Or force: changelog, release-body, prs. |
healthcheck |
No | false |
Validate LLM API key with a minimal probe request before synthesis. |
floating-tags |
No | false |
Update floating major version tags (e.g., v1) after release. |
webhook-url |
No | "" |
Webhook endpoint URL. On synthesis success, POST a JSON payload with version, notes (markdown/HTML/plaintext), and release URL. |
webhook-secret |
No | "" |
HMAC-SHA256 secret for signing webhook payloads (X-Signature-256 header). Optional. |
slack-webhook-url |
No | "" |
Slack Incoming Webhook URL. On synthesis success, POST a Block Kit message with version, categorized notes, and release link. |
rss-feed-file |
No | manifest, then "" |
Update this RSS 2.0 feed file with each release (includes synthesized notes as HTML). The feed file is committed back to the repo. |
rss-max-entries |
No | 50 |
Maximum number of items retained in rss-feed-file. |
* llm-api-key is required when synthesis: true and the model policy does
not skip the LLM call.
| Output | Description |
|---|---|
released |
true if a new release/tag was created, otherwise false. |
release-tag |
Tag created by semantic-release (empty if no release). |
synthesis-succeeded |
true when synthesis/update succeeds or when policy intentionally skips LLM synthesis for the released tag. |
synthesis-quality |
valid, degraded, ungrounded, skipped, or failed. |
synthesis-status |
Compact JSON status with quality, failure stage/message, model attempts, context sources, cost estimate, release classification, and publication destination outcomes. |
release-notes |
Synthesized user-facing release notes markdown. Empty if synthesis was skipped or failed. |
webhook-sent |
true when the generic webhook notification was sent successfully. |
slack-sent |
true when the Slack notification was sent successfully. |
synthesis-failure-issue-action |
Companion failure-issue lifecycle result: closed, reported, failed, or skipped. |
Landmark separates the semantic-release publish step from its owned synthesis and distribution steps:
synthesis-required: "true"treats failed or degraded synthesis as a hard failure and blocks release-body mutation and floating-tag movement.- Optional synthesis still allows the release to exist, but partial Landmark
failures are reported through
synthesis-succeeded: falseand protected outputs such as floating tags do not move unless synthesis and release-body update both succeed. - Every synthesized section is checked against the release's deterministic
commit range before it can ship:
## Breaking Changesand## Bug Fixesheadings must be backed by at least one commit carrying that exact signal (aBREAKING CHANGE/!:marker, or afix:commit), and every other section must be backed by at least one commit in the release at all. A section with zero matching commits issynthesis-quality: ungrounded— this is a hard failure regardless ofsynthesis-required, because publishing invented release notes is never an acceptable degraded-quality tradeoff.synthesizeretries configured fallback models before giving up, and writes a claim-to-source map (--claim-map-file, step outputclaim_map_file) recording which sections matched which commits, so the rejection is auditable rather than silent. Seereplay-action --scenario synthesis_fabrication_gatefor the canary v1.14.0 regression fixture this gate exists to catch (one realfeatPR; the model invented Breaking Changes and Bug Fixes sections describing changes that never shipped, and the old shape-only validator scored itvalid). - Intentional synthesis skips from
model.policy: off, low-significance balanced policy, or manifest budget limits are treated as successful policy outcomes. Release-body mutation and artifact writes are skipped, whilesynthesis-status.context.costrecords the reason. - External GitHub, webhook, release-feed, Slack, and LLM calls made by the Rust
runtime use bounded curl calls (
--connect-timeout,--max-time, retries for 429/5xx).replay-action --scenario http_resilience_policyexercises slow, throttled, and failing providers,replay-action --scenario release_feed_adapterexercises the signed release-kit receiver path, andreplay-action --scenario action_side_effect_coveragefails ifaction.ymlinvokes a Landmark subcommand without replay coverage. - Generated
release-notesoutput uses a collision-resistant GitHub output delimiter so synthesized content cannot truncate the output payload.
- uses: misty-step/landmark@v0
with:
github-token: ${{ steps.release-token.outputs.token }}
llm-api-key: ${{ secrets.OPENROUTER_API_KEY }}- uses: misty-step/landmark@v0
with:
github-token: ${{ steps.release-token.outputs.token }}
llm-api-key: ${{ secrets.OPENAI_API_KEY }}
llm-model: gpt-4o
llm-api-url: https://api.openai.com/v1/chat/completions- uses: misty-step/landmark@v0
with:
github-token: ${{ steps.release-token.outputs.token }}
llm-api-key: ${{ secrets.PROVIDER_API_KEY }}
llm-model: provider/model-id
llm-api-url: https://provider.example.com/v1/chat/completionsBackfill is a Rust-owned CLI path for mature repositories that already have tags, GitHub Releases, or a CHANGELOG.md. It plans historical release-note artifacts without calling the LLM, then can write the same markdown, plaintext, HTML, JSON, and RSS-compatible artifact formats used by normal synthesis.
Preview the migration from an existing tag:
landmark backfill --repo-root . --since v1.0.0 --dry-runWrite portable artifacts only, which is the default safe migration mode:
landmark backfill \
--repo-root . \
--since v1.0.0 \
--mode artifacts-only \
--repository owner/repo \
--github-token "$GITHUB_TOKEN"Preview GitHub Release body updates before considering mutation:
landmark backfill \
--repo-root . \
--since v1.0.0 \
--mode release-body \
--dry-run \
--repository owner/repo \
--github-token "$GITHUB_TOKEN"release-body writes are refused unless the run is a dry-run or the operator passes --confirm-release-body. The output manifest lists processed tags, skipped tags, remaining tags, artifact paths, preview hashes, and the estimated cost. Artifact backfill does not call the LLM; use the manifest to batch later synthesis if you want enhanced historical notes.
For private repos where GitHub Releases aren't publicly visible, use artifact outputs to make notes portable:
- name: Run Landmark
id: landmark
uses: misty-step/landmark@v0
with:
github-token: ${{ steps.release-token.outputs.token }}
llm-api-key: ${{ secrets.OPENROUTER_API_KEY }}
notes-output-file: docs/releases/{version}.md
notes-output-text-file: docs/releases/{version}.txt
notes-output-html-file: docs/releases/{version}.html
notes-output-json: releases.jsonThis writes per-version markdown files and maintains a typed JSON artifact feed for changelog pages:
[
{
"version": "1.2.0",
"tag": "v1.2.0",
"notes": "## New Features\n- ...",
"markdown": "## New Features\n- ...",
"plaintext": "New Features\n...",
"html": "<h2>New Features</h2>\n<ul>\n<li>...</li>\n</ul>",
"slack": "## New Features\n- ...",
"sections": [
{
"title": "New Features",
"bullets": [
{
"text": "...",
"links": []
}
]
}
],
"published_at": "2026-02-08T12:00:00Z"
}
]The synthesis-status output is a compact JSON object for automation:
{
"synthesis_enabled": true,
"released": true,
"succeeded": true,
"quality": "valid",
"failure_stage": "",
"failure_message": "",
"model_attempts": [
{
"model": "deepseek/deepseek-v4-flash-0731",
"succeeded": true,
"quality": "valid",
"message": "",
"cost": {
"input_tokens": 1800,
"output_tokens": 1000,
"model_tier": "balanced",
"model": "deepseek/deepseek-v4-flash-0731",
"estimated_usd": 0.00118,
"skip": false,
"skip_reason": ""
}
}
],
"context": {
"release": {
"version": "v1.2.0",
"changelog_source": "auto",
"model_policy": "balanced"
},
"deterministic": {
"commits": [
{
"subject": "feat(cli): add import",
"body": "Adds a guided import flow.",
"short_hash": "abc1234",
"conventional_type": "feat",
"breaking": false
}
],
"tags": ["v1.1.0"],
"changed_files": ["src/import.rs"],
"diff_stats": [{ "path": "src/import.rs", "additions": 42, "deletions": 3, "binary": false }],
"docs": [{ "path": "README.md", "title": "Landmark" }],
"artifacts": {
"internal_technical_changelog": "landmark.internal-technical-changelog.v1",
"public_release_notes": "landmark.public-release-notes.v1:developer"
}
},
"sources": [
{ "name": "prompt_template", "kind": "prompt", "estimated_tokens": 700, "included": true },
{ "name": "technical_changelog", "kind": "auto", "estimated_tokens": 900, "included": true },
{ "name": "product_manifest", "kind": "manifest", "estimated_tokens": 40, "included": true }
],
"classification": {
"categories": ["user-visible"],
"significance": "medium",
"user_visible": true,
"breaking": false,
"security": false,
"migration_heavy": false,
"source": "model",
"model": "deepseek/deepseek-v4-flash",
"deterministic_signals": ["conventional:feat"],
"disagreements": [],
"reasons": ["model classified the parsed release evidence as user-visible"]
},
"cost": {
"input_tokens": 1800,
"output_tokens": 1000,
"model_tier": "balanced",
"model": "deepseek/deepseek-v4-flash-0731",
"estimated_usd": 0.00118,
"skip_reason": ""
},
"decision": {
"action": "used",
"reason": "balanced policy uses balanced model tier",
"llm_required": true,
"model_tier": "balanced"
}
},
"destinations": {
"release_body": { "enabled": true, "succeeded": true, "failure_stage": "", "failure_message": "" },
"artifacts": { "enabled": true, "succeeded": true, "failure_stage": "", "failure_message": "" },
"rss": { "enabled": false, "succeeded": false, "failure_stage": "", "failure_message": "" },
"webhook": { "enabled": false, "succeeded": false, "failure_stage": "", "failure_message": "" },
"slack": { "enabled": false, "succeeded": false, "failure_stage": "", "failure_message": "" }
}
}To publish a simple RSS 2.0 release feed (for feed readers, docs sites, etc.), set rss-feed-file:
- uses: misty-step/landmark@v0
with:
github-token: ${{ steps.release-token.outputs.token }}
llm-api-key: ${{ secrets.OPENROUTER_API_KEY }}
rss-feed-file: docs/releases.xml
rss-max-entries: "50"Landmark updates the feed on each synthesized release and commits the file back to your repo.
For automatic Slack notifications, set slack-webhook-url.
The release-notes output is still available for custom notifications:
- name: Custom Notify Slack
if: steps.landmark.outputs.released == 'true'
run: |
echo "${{ steps.landmark.outputs.release-notes }}" | post-to-slackLandmark releases itself without pushing generated release commits directly to
protected master. The repository workflow has two phases:
prepare-release-prrunscargo run --locked -p landmark -- prepare-self-release, updatesCHANGELOG.md,package.json,crates/landmark/Cargo.toml, andCargo.lockonlandmark/self-release. It then opens or updates a release PR, which must pass the normalmerge-gatebefore it can land.publish-landed-releaseruns onmasterpushes. It publishes a GitHub Release only when landed metadata is ahead of the latest semver tag.build-release-assetsthen builds the release binary for each supported target (x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,aarch64-apple-darwin,x86_64-apple-darwin) andpublish-release-assetsuploads them plus achecksums.txtto the GitHub Release, then runs Landmark insynthesis-onlymode to update the release body and floating major tag. That synthesis pass is non-blocking because the release has already been published; failed or degraded synthesis is surfaced through Landmark outputs without turning a published release into a failed deployment. The floating major tag (and any@v1-pinned consumer) only moves once release assets are live, so consumers never resolve a tag whose binary is not yet downloadable.
The local replay oracle for this path is:
cargo run --locked -p landmark -- replay-action \
--evidence-dir .landmark/replay \
--scenario self_release_pr_pathThis repository keeps package.json and the Rust crate version aligned to release tags:
prepare-self-releaseupdatespackage.json,crates/landmark/Cargo.toml, andCargo.lockbefore opening the release PR..releaserc.jsonstill runscargo run --locked -p landmark -- update-version-metadatafor consumers using full semantic-release mode.- The release commit includes
CHANGELOG.md,package.json,crates/landmark/Cargo.toml, andCargo.lock. - CI runs
cargo run --locked -p landmark -- check-version-syncto fail fast when metadata drifts from the latest semver tag.
The public action contract is checked from action.yml:
cargo run --locked -p landmark -- check-action-contractfails when the README inputs table diverges from action metadata.- The same command scans examples, project docs, and release workflows for unknown or deprecated Landmark inputs.
- CI runs the contract check before tests so stale consumer instructions fail fast.
Landmark's replay harness creates disposable git fixture repositories and fake local GitHub/LLM endpoints, then exercises synthesis, release-body updates, artifact writing, failure policy, and floating-tag behavior without production secrets:
bin/replay-action --evidence-dir .landmark/replayThe command writes .landmark/replay/replay-result.json with action outputs,
generated notes, release body before/after state, git tags, structured logs, and
fake service requests. CI runs a bounded replay on pull requests, the full replay
on master, and uploads the evidence packet for inspection.
The diff-grounded version-decision scenarios are:
semver_evidence_agrees, semver_evidence_upgrades,
semver_evidence_absent, and semver_evidence_tool_failure. They use
disposable fixture repos and a fake cargo semver-checks binary so the replay
proves Landmark's reconciliation and evidence schema without depending on a
developer machine's installed cargo subcommands.
For a local one-command gate, run:
bin/gateFor branch protection, require these hosted checks:
merge-gate: aggregate gate for thelocal-gatejob that runsbin/gate.trufflehog: repository secret scan.
Landmark ships a default config at configs/.releaserc.json. If your repo has its own semantic-release config file (.releaserc, .releaserc.json, .releaserc.yml, .releaserc.yaml, release.config.js, release.config.cjs, or release.config.mjs), Landmark uses it instead of the bundled defaults.
This lets you customize branches, plugins, commit-analyzer rules, or anything else semantic-release supports.
If no config file is found, Landmark falls back to its bundled config with:
@semantic-release/commit-analyzer@semantic-release/release-notes-generator@semantic-release/changelog@semantic-release/git@semantic-release/github
CHANGELOG.md is fully managed by @semantic-release/changelog. Do not keep a manual # Changelog or ## [Unreleased] section in this repository, or release entries will be duplicated/mixed.
Landmark resolves the synthesis prompt template in this order:
- Explicit input —
prompt-template-path: my-templates/release.md - Convention —
.landmark/synthesis-prompt.mdin your repo root - Bundled audience variant — Landmark's built-in template selected by
audience(defaultgeneral)
Built-in audience variants:
| Audience | Template |
|---|---|
general |
templates/prompts/general.md |
developer |
templates/prompts/developer.md |
end-user |
templates/prompts/end-user.md |
enterprise |
templates/prompts/enterprise.md |
Custom templates must include these variables:
| Variable | Value |
|---|---|
{{PRODUCT_NAME}} |
Repository or product name |
{{VERSION}} |
Release version/tag |
{{TECHNICAL_CHANGELOG}} |
Extracted changelog content |
Optional variables (supported, not required):
| Variable | Value |
|---|---|
{{BULLET_TARGET}} |
Suggested bullet range (for example, 3-7) |
{{BREAKING_CHANGES_SECTION}} |
Breaking-change candidates extracted from the technical changelog (empty when none) |
{{PRODUCT_CONTEXT}} |
Optional ## Product context section (from product-description) |
{{VOICE_GUIDE}} |
Optional ## Voice guide section (from voice-guide) |
See templates/synthesis-prompt.md or templates/prompts/general.md as a starting point for your own template.
Technical release notes (generated):
### Features
- add workspace import command (#214)
### Bug Fixes
- handle retries when webhook signature is stale (#229)
### Chores
- bump ci cache keySynthesized ## What's New section (prepended):
## What's New
## New Features
- Import workspace configuration in one command, cutting setup time.
## Bug Fixes
- Webhook deliveries now retry more reliably when signatures expire.Landmark intentionally omits internal-only changes (CI/tooling) from user-facing summaries.