-
Notifications
You must be signed in to change notification settings - Fork 9
feat(release): automate per-crate releases and version contract artifacts (ENG-522) #528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
fa0836d
ea0a635
9d44774
6895f2b
85d6a66
0241c84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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 | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On the release-tag runner this job only installs the Rust toolchain with 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For tags that match this workflow but are not NEAR artifacts in Useful? React with 👍 / 👎. |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 -SRepository: 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 🤖 Prompt for AI Agents |
||
| # 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 }} | ||
There was a problem hiding this comment.
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 uploadsdist/*. Soroban package tags and legacy blobs therefore fail after reporting that they were intentionally skipped. Set a step output only after creatingdist/, and gate the upload step on it.Also applies to: 108-112, 133-137
🤖 Prompt for AI Agents