Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: PR Title

# PRs land on `dev` as squash merges, so the PR title becomes the commit message
# on the default branch. release-plz reads those messages to decide each crate's
# next version, which makes the title load-bearing: an unconventional title means
# the touched crates get no version bump and no changelog entry.
#
# Only the *type* is enforced. Scope is conventional but free-form on purpose —
# release-plz selects which crate to bump from the files a commit touches, not
# from the scope, so an allowlist would add friction without affecting releases.
#
# See the "Commit and PR titles" section of AGENTS.md for the full convention.

# `pull_request`, not `pull_request_target`: the title is already in the event
# payload, so there is no reason to run in the base-repo context with secrets on
# a public repo. Nothing here checks out PR code.
on:
pull_request:
types: [opened, edited, reopened]

permissions:
pull-requests: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
validate:
name: Validate conventional commit title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Keep in sync with the `commit_parsers` in `release-plz.toml`.
types: |
feat
fix
perf
refactor
docs
test
ci
build
chore
deploy
requireScope: false
# Guard the two mistakes that actually break version inference:
# a Linear ID used as the summary, or an empty/placeholder subject.
# (`ENG-504: Nest governance...` as a *title prefix* is not a valid
# type, so it is already rejected — the ID belongs in the PR body.)
subjectPattern: ^(?![A-Z]{2,}-\d+).+$
subjectPatternError: |
The subject "{subject}" of PR title "{title}" is invalid.
Put the Linear issue ID in the PR description, not the title —
a title like "ENG-123: do the thing" breaks release version
inference. Write "feat(gateway): do the thing" instead.
15 changes: 14 additions & 1 deletion .github/workflows/publish-soroban-vault-cli.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
name: Publish Soroban Vault CLI Image

# Fires automatically on the crate's release tag, so the published image version
# is the version release-plz actually released rather than free text someone
# typed. `workflow_dispatch` is kept for ad-hoc/dev images.
on:
push:
tags:
- "templar-soroban-vault-cli-v*"
workflow_dispatch:
inputs:
tag:
Expand All @@ -14,7 +20,7 @@ on:
default: false
type: boolean

run-name: Publish Soroban Vault CLI image (${{ inputs.tag }})
run-name: Publish Soroban Vault CLI image (${{ inputs.tag || github.ref_name }})

concurrency:
group: publish-soroban-vault-cli-${{ github.ref }}
Expand All @@ -41,6 +47,13 @@ jobs:
run: |
set -euo pipefail

# On a release tag, derive the image tag from the version
# (`templar-soroban-vault-cli-v1.2.0` -> `1.2.0`); otherwise use the
# dispatch input.
if [[ -z "${INPUT_TAG:-}" ]]; then
INPUT_TAG="${GITHUB_REF_NAME##*-v}"
fi

if [[ ! "$INPUT_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
echo "Invalid Docker tag: $INPUT_TAG" >&2
exit 1
Expand Down
137 changes: 137 additions & 0 deletions .github/workflows/release-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Release Artifacts

# Proves that a released contract blob is genuinely reproducible: rebuilds the
# contract in the pinned NEP-330 Docker image and fails unless the result is
# byte-for-byte identical to the blob checked in under
# `contract/artifacts/res/near/<target>/<version>/`.
#
# The rebuild happens at the release's recorded `source_commit`, NOT at the tag.
# `cargo near build reproducible-wasm` embeds the source commit into the WASM
# (NEP-330), so the same source built at two different commits produces
# different bytes — and a blob is necessarily committed *before* the tag that
# releases it exists. Verifying at the tag would therefore fail every time.
#
# This is what makes a checked-in blob *canonical* rather than "whatever some
# laptop produced", and it is what lets anyone verify the on-chain code against
# this source tree. The fast, build-free catalog checks run on every PR via
# `test.yml` → `artifact-drift-check`; this one is heavy (Docker + a full
# optimized WASM build), so it is reserved for release tags.
#
# Also uploads each verified blob plus a `checksums.txt` to the GitHub Release,
# so ops and auditors can fetch bytes without cloning the repo.

on:
push:
tags:
# Broad glob: catalog membership is decided in `ids.rs`, not here. A
# tagged package the catalog doesn't know about fails fast at the
# `--print-metadata` lookup below.
- "*-contract-v*"

permissions:
contents: read

env:
CARGO_TERM_COLOR: always
SQLX_OFFLINE: "true"

jobs:
verify:
name: Verify reproducible build
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write # attach assets to the GitHub Release
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
# `cargo near build reproducible-wasm` builds from committed git
# state and reads the repository, so the full checkout must be intact.
fetch-depth: 0
persist-credentials: false

# Not `./.github/actions/rust`: the WASM build runs inside the
# cargo-near Docker image, so a host rust-cache entry for this job would
# be cold on every release tag and just add eviction pressure.
- uses: dtolnay/rust-toolchain@dd44c20b1206a46e25fba8503d5d7c9a33bd355a
with:
toolchain: "1.86.0"

- name: Resolve artifact from tag
id: resolve
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# `templar-market-contract-v1.4.0` -> package `templar-market-contract`, version `1.4.0`
package="${REF_NAME%-v*}"
version="${REF_NAME##*-v}"
{
echo "package=${package}"
echo "version=${version}"
} >> "$GITHUB_OUTPUT"

- name: Rebuild and compare against the checked-in blob
env:
PACKAGE: ${{ steps.resolve.outputs.package }}
VERSION: ${{ steps.resolve.outputs.version }}
run: |
set -euo pipefail

# Ask the catalog for this package's source path and target name
# rather than re-deriving them here and drifting from ids.rs.
if ! metadata=$(cargo run --quiet -p templar-contract-artifacts \
--features workspace-loader,clap --bin prebuild-test-contracts -- \
--print-metadata --artifact "$PACKAGE" 2>/dev/null); then
echo "::notice::${PACKAGE} has no catalogued WASM artifact (e.g. a" \
"Soroban contract); nothing to verify."
exit 0
Comment on lines +86 to +89

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Skip the upload when verification is intentionally skipped.

Both supported early-exit paths leave no dist/ directory, but the next step unconditionally uploads dist/*. Soroban package tags and legacy blobs therefore fail after reporting that they were intentionally skipped. Set a step output only after creating dist/, and gate the upload step on it.

Also applies to: 108-112, 133-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-artifacts.yml around lines 86 - 89, Update the
release workflow’s verification step to create and set a step output only when
dist/ is created and artifact verification should proceed; leave it unset on the
intentional early-exit paths for Soroban contracts and legacy blobs. Add an if
condition to the upload step so it runs only when that output indicates
verification produced dist/ contents, preventing unconditional dist/* uploads.

fi
read -r source_path target _current_version <<<"$metadata"

committed="contract/artifacts/res/near/${target}/${VERSION}/${target}.wasm"
if [[ ! -f "$committed" ]]; then
echo "::error::${PACKAGE} ${VERSION} was tagged but no blob is checked in at ${committed}." >&2
echo "Run 'just artifact-release' and commit the blob with its ids.rs entry." >&2
exit 1
fi

# The blob-vs-pin check already runs on every PR via
# `check-artifact-drift.sh`; this job's unique job is the rebuild.
source_commit=$(
cargo run --quiet -p templar-contract-artifacts \
--features workspace-loader,clap --bin prebuild-test-contracts -- \
--print-release "$VERSION" --artifact "$PACKAGE"
)

if [[ -z "$source_commit" ]]; then
echo "::notice::${PACKAGE} ${VERSION} is a legacy blob with no recorded" \
"source_commit (e.g. bytes recovered from chain), so it cannot be" \
"reproducibly rebuilt. Skipping."
exit 0
fi

# Rebuild at the commit the blob was actually built from. The tag
# commit would embed a different NEP-330 snapshot and never match.
git worktree add ../verify "$source_commit"
(cd ../verify && cargo near build reproducible-wasm --manifest-path "${source_path}/Cargo.toml")
Comment on lines +117 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Install cargo-near before invoking cargo near

On the release-tag runner this job only installs the Rust toolchain with dtolnay/rust-toolchain; unlike the existing node test workflow, it never installs the cargo-near subcommand before calling cargo near build reproducible-wasm. Any catalogued release with a source_commit will therefore fail with Cargo's “no such command: near” instead of verifying or uploading the artifact.

Useful? React with 👍 / 👎.


rebuilt="../verify/target/near/${target}/${target}.wasm"
if ! cmp -s "$rebuilt" "$committed"; then
echo "::error::Reproducible build at ${source_commit} does not match the checked-in blob." >&2
echo " committed: $(sha256sum "$committed" | cut -d' ' -f1)" >&2
echo " rebuilt: $(sha256sum "$rebuilt" | cut -d' ' -f1)" >&2
exit 1
fi
echo "Reproducible: ${target} ${VERSION} matches byte-for-byte at ${source_commit}."

mkdir -p dist
cp "$committed" "dist/${target}-${VERSION}.wasm"
(cd dist && sha256sum ./*.wasm > checksums.txt)

- name: Attach artifacts to the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF_NAME: ${{ github.ref_name }}
run: gh release upload "$REF_NAME" dist/* --clobber
Comment on lines +133 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip uploads when no artifact was produced

For tags that match this workflow but are not NEAR artifacts in ArtifactId::ALL—for example the Soroban contract packages that release-plz will tag by default—the metadata step exits successfully before creating dist/, but this unconditional upload still runs and fails on dist/*. Either narrow the tag trigger or gate the upload step on an output that says a blob was actually copied.

Useful? React with 👍 / 👎.

97 changes: 97 additions & 0 deletions .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Release-plz

# Two jobs, both on every push to `dev`:
#
# release-plz-pr opens/updates one standing "chore: release" PR carrying the
# pending version bumps and CHANGELOG entries.
# release-plz runs after that PR is merged: tags each released crate and
# cuts its GitHub Release. Tier configuration lives in
# `release-plz.toml`.
#
# Merging the Release PR is the release trigger — `release_always = false` means
# an ordinary merge to `dev` ships nothing.
#
# Secrets:
# RELEASE_PLZ_TOKEN optional PAT. A PR opened with the default
# GITHUB_TOKEN does NOT trigger other workflows, so
# without this the Release PR never runs `test.yml` and
# you would be merging an unverified release. Falls back
# to GITHUB_TOKEN so the workflow still works unattended.
# CARGO_REGISTRY_TOKEN only consulted once crates leave `git_only` mode.
Comment on lines +15 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## release-plz.yml\n'
cat -n .github/workflows/release-plz.yml | sed -n '1,220p'

printf '\n## release-artifacts.yml\n'
cat -n .github/workflows/release-artifacts.yml | sed -n '1,220p'

printf '\n## release workflow refs\n'
rg -n "RELEASE_PLZ_TOKEN|GITHUB_TOKEN|workflow_run|release-artifacts|release-plz|release tags|tag" .github/workflows -S

Repository: Templar-Protocol/contracts

Length of output: 17001


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for any documentation or comments that state how release-plz tokens are expected to behave.
rg -n "RELEASE_PLZ_TOKEN|release-plz|release artifacts|GITHUB_TOKEN.*trigger|does NOT trigger|workflow_run|tag-triggered" -S .

Repository: Templar-Protocol/contracts

Length of output: 2836


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n RELEASING.md | sed -n '88,108p'

Repository: Templar-Protocol/contracts

Length of output: 1242


Require RELEASE_PLZ_TOKEN here. The GITHUB_TOKEN fallback suppresses the release PR’s downstream test.yml run and the tag-triggered release-artifacts.yml check, so a release can ship without those gates. Make the PAT mandatory instead of falling back silently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-plz.yml around lines 15 - 20, Update the
RELEASE_PLZ_TOKEN configuration documentation and usage in the release workflow
to require the PAT explicitly, removing the fallback to GITHUB_TOKEN. Ensure the
workflow fails clearly when RELEASE_PLZ_TOKEN is unset rather than proceeding
unattended with reduced release validation.

# Publishing is currently blocked upstream — see the
# header of `release-plz.toml`.

on:
push:
branches: [dev]

permissions:
contents: read

env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: "0"
# The workspace has sqlx macros; without this, any cargo invocation that
# builds `service/relayer` or `gateway/store` wants a live database.
SQLX_OFFLINE: "true"

jobs:
release-plz-release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write # create tags and GitHub Releases
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
# release-plz derives each crate's current version from its git tags,
# so it needs the full history, not a shallow clone.
fetch-depth: 0
persist-credentials: false

# No wasm is compiled on the host in these jobs.
- uses: ./.github/actions/rust
with:
targets: ""

- name: Run release-plz
uses: release-plz/action@2eb1d8bcb770b4c48ccfaad919734b38b51958c9 # v0.5.131
with:
command: release
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

release-plz-pr:
name: Release PR
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
# Never let two runs race on the same release branch: one would force-push
# over the other's bumps. Queue instead of cancelling, so the last push to
# `dev` is always reflected in the PR.
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
persist-credentials: false

# No wasm is compiled on the host in these jobs.
- uses: ./.github/actions/rust
with:
targets: ""

- name: Run release-plz
uses: release-plz/action@2eb1d8bcb770b4c48ccfaad919734b38b51958c9 # v0.5.131
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
23 changes: 20 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ Use this section as an execution checklist: read the local docs first, preserve
- Preserve existing crate structure and naming patterns unless there is a strong reason to change them.
- This codebase is security-sensitive. Review changes with an auditor mindset, especially in smart contracts and cross-contract flows.

## Commit And PR Titles

PRs are squash-merged, so **the PR title becomes the commit message on `dev`**, and `release-plz` reads those messages to decide each crate's next version. A title that does not parse means the crates you touched get **no version bump and no changelog entry**. `.github/workflows/pr-title.yml` enforces this on every PR.

Format: `type(scope): summary`

- **Allowed types** — `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `ci`, `build`, `chore`, `deploy`. Keep this list in sync with `commit_parsers` in `release-plz.toml` and `types` in `.github/workflows/pr-title.yml`.
- **Version impact** — `fix` bumps patch, `feat` bumps minor, and a breaking change bumps major. Everything else is recorded but does not itself force a bump.
- **Breaking changes** — mark with `!` after the scope (`feat(gateway)!: drop the legacy read path`) or a `BREAKING CHANGE:` footer in the PR body. This is the only signal that produces a major bump; forgetting it ships a breaking change as a minor.
- **Scope is optional and free-form.** It is conventional to name the crate or area (`gateway`, `market`, `vault`, `relayer`, `proxy-oracle`, `manager`), but nothing enforces a fixed list: release-plz determines *which* crate to bump from the files a commit touches, not from the scope.
- **Linear IDs go in the PR description, never the title.** `ENG-504: Nest governance under proxy-oracle` is not a valid type and will be rejected — write `refactor(proxy-oracle): nest governance` and reference `ENG-504` in the body.

Releases themselves are cut by merging the standing "chore: release" PR. See `RELEASING.md`.

## Build And Test

- Format: `cargo fmt`
Expand All @@ -87,8 +101,10 @@ Notes:
- The sandbox gate derives test selection and Cargo package narrowing from one package classification. See "Cross-Cutting Lists To Keep In Sync" below before adding a node-backed crate. If these tests fail because no neard is available, say that clearly instead of silently skipping them.
- `cargo test -p templar-common --lib` is a good fast regression check for logic changes in `common`.
- Node-backed tests need the contract Wasms prebuilt; rebuilding WASM inside each run is much slower. `just test-sandbox` sources `script/sandbox-up.sh`, which prebuilds them and exports `TEST_CONTRACTS_PREBUILT=1`. If you run a node test by hand outside that recipe (e.g. a plain `cargo test` in owned mode), do the same first. `--stale` works by setting that same variable on the way in: the script then only verifies the artifacts exist (`prebuild-test-contracts --check`) and fails before booting nodes if any are missing.
- Run `./script/check-artifact-drift.sh` when validating checked-in embedded WASM blobs; it is a pure hash/version check (no builds) that verifies each blob matches its pinned `expected_sha256` and catalog version.
- Embedded contract blobs under `contract/artifacts/res/near/` are pinned _release_ artifacts, NOT a mirror of source. **A contract source change does NOT refresh its blob, and no CI check will flag the blob as stale** (the drift check compares blob-vs-pin and version-vs-`Cargo.toml`, never blob-vs-source). When — and only when — you intend a contract source change to become what the gateway deploys, refresh its blob by following `contract/artifacts/README.md` ("⚠️ Refreshing a checked-in blob"): on a clean committed tree, `cargo near build reproducible-wasm --manifest-path <source_path>/Cargo.toml`, copy the output into `res/near/`, update that entry's `expected_sha256` (+ `version`) in `contract/artifacts/src/ids.rs`, and commit them together. Unreleased work-in-progress is meant to lag the blob — do not refresh reflexively.
- Run `./script/check-artifact-drift.sh` when validating checked-in WASM blobs; it is a pure, build-free check (seconds) covering per-release hash pins, newest-release-vs-`Cargo.toml`, release-list well-formedness, and catalog-vs-disk agreement.
- Contract blobs under `contract/artifacts/res/near/<target>/<version>/` are **immutable released artifacts**, one directory per released version. Shipping new bytes means *adding* a release, never editing one — historical blobs back the migration and upgrade tests, so changing one silently invalidates them.
- Source is allowed to move ahead of the newest released blob; unreleased work-in-progress is *meant* to lag it. The tripwire is the crate version: bump a contract's `Cargo.toml` version and the drift check fails until you cut the matching blob with `just artifact-release <id>` (reproducible build from the committed tree → new `res/near/` directory → new entry in `contract/artifacts/src/ids.rs`). Commit the blob and its catalog entry together.
- Reproducibility (do the bytes match what the source actually compiles to?) is verified on release tags by `.github/workflows/release-artifacts.yml`, which rebuilds and requires a byte-for-byte match.

## Cross-Cutting Lists To Keep In Sync

Expand All @@ -98,8 +114,9 @@ Several CI/test-infra files enumerate crates, contracts, or paths by hand. A fea
- `justfile` — add the package to `sandbox_full_packages`; the sandbox filter and Cargo package boundary are both derived from that list. (`templar-gateway-service`'s node tests live in `src/` and are matched separately by module path.)
- `.github/workflows/test.yml` — add the crate's `src/**` and `tests/**` under the `changes` job's `near_integration` paths filter, or the test job won't trigger on changes to it.
- **Adding or removing a contract / mock WASM**:
- `contract/artifacts/src/ids.rs` — the `ArtifactId` enum and its embedded `res/near/...` blob (see `contract/artifacts/README.md`). This is the canonical list; `script/prebuild-test-contracts.sh` derives from it.
- `contract/artifacts/src/ids.rs` — the `ArtifactId` enum, the catalog entry's `releases` list, an `embedded_bytes_for_version` arm per release, and the `embedded_bytes` arm for the newest one (see `contract/artifacts/README.md`). This is the canonical list; `script/prebuild-test-contracts.sh` derives from it.
- `gateway/testing/src/wasm.rs` — the `wasm_fns!` list, so the harness can load it.
- **Releasing a new version of an existing contract**: bump its `Cargo.toml` version, then `just artifact-release <id>` — the drift check stays red until the blob and its catalog entry land together. See `RELEASING.md`.
- **Adding a new top-level source area / crate**:
- `.github/workflows/test.yml` paths groups (`near_integration`, `soroban`, `feature_matrix`, `artifact_manifests`) and `.github/workflows/gas-report.yml` paths — add it to the right group so the relevant jobs fire.
- Root `Cargo.toml` `[workspace] members` if an existing glob (`gateway/*`, `tools/*`, …) doesn't already cover the path.
Expand Down
Loading
Loading