Skip to content

fix: declare @spacedevin/deck as a peer of both packages #59

fix: declare @spacedevin/deck as a peer of both packages

fix: declare @spacedevin/deck as a peer of both packages #59

Workflow file for this run

# Run tests and on main push create prerelease with npm package.
# When you "Set as latest release" (uncheck prerelease), the npm-release workflow publishes to npm.
#
# Version bumps use tishlang/sem (@tishlang/sem) — Conventional Commits drive semver
# (feat/fix/perf/BREAKING release; chore/docs/ci do not). Config: .semrc.json
name: CI (test, prerelease)
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Conformance corpus (JS)
run: npm run test:conformance
# The third target. The JS build and the Rust crate were both checked against the corpus; the
# Tish source itself was not, so "one source, three targets" had a leg missing.
- name: Conformance corpus (Tish)
run: npm run test:conformance:tish
- name: Examples
run: npm run examples
- name: Build
run: npm run build
# The player is the host half: Web Audio, transport, host defaults/clamps. It builds against
# the language package IN THIS REPO (a file: link), so a grammar change that breaks playback
# fails here rather than after publish.
- name: Player (@spacedevin/deck-player)
run: npm test -w @spacedevin/deck-player
# The Rust crate is emitted from the SAME src/index.tish as dist/deck.js, which is what stops the
# JS host and tish-gba's build-time bake from drifting. Nothing verified that: a change that broke
# the rust-lib emit, or that made the two targets parse differently, would have gone unnoticed
# until release — or until a consumer hit it. `cargo test` here runs the same conformance corpus
# the JS build is checked against, so both targets are held to one contract on every PR.
rust_crate:
name: Rust crate (deckfile)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
run: npm ci
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
workspaces: crate
- name: Emit the crate
run: |
export PATH="$PWD/node_modules/.bin:$PATH"
npm run build:rust
- name: Conformance corpus (from Rust)
working-directory: crate
run: cargo test
# A crate that cannot be packaged cannot be released; catch that on the PR, not at publish.
#
# The dry run leaves an extracted copy of the crate at `target/package/deckfile-<version>/`.
# rust-cache's post step prunes `target/` before saving it, walks into that copy, and dies on
# the trybuild layout it expects but this crate does not have:
#
# ENOENT: opendir '…/crate/target/package/deckfile-0.1.0/tests/trybuild'
#
# Four `##[error]` annotations on an otherwise green run. It only shows up on a cache MISS,
# since rust-cache skips the prune entirely on a hit — which is why it appears sporadically,
# whenever a change to the emitted crate moves the cache key. Nothing should be caching the
# output of a dry-run publish, so drop it before the post step can find it.
- name: Packaging check
working-directory: crate
run: |
cargo publish --dry-run --allow-dirty
rm -rf target/package
# ── Would this land a release? Runs on PRs so a bad commit type is caught before merge. ───────
release_check:
name: Release check (sem dry-run)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: "22" }
- name: Dry-run sem
id: sem
uses: tishlang/sem@v1
with:
dry_run: true
config: |
branches: [main]
plugins:
- "@sem/commit-analyzer"
- "@sem/release-notes-generator"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Report bump
run: |
if [ "${{ steps.sem.outputs.new_release_published }}" = "true" ]; then
echo "::notice::Would release ${{ steps.sem.outputs.new_release_version }} from these commits."
elif [ "${{ steps.sem.outputs.skipped }}" = "true" ]; then
echo "::notice::Soft-skipped (${{ steps.sem.outputs.skip_reason }})."
else
echo "::notice::No version bump. Conventional Commits drive semver — feat/fix/perf/BREAKING release, chore/docs/ci do not."
fi
release:
name: Prerelease
needs: [test, rust_crate, release_check]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
# @sem/github opens a failure issue when a release throws. Without this it cannot, and the
# real error is buried under a 403 from the issues API.
issues: write
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: "22" }
- name: Install dependencies
run: npm ci
# A tag is a commit-shaped object and actions/checkout configures no identity, so sem dies with
# "Failed to create tag: Committer identity unknown" without this. chuggie-engine shipped zero
# releases for months on exactly that.
- name: Configure git identity for the release tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Next version via sem (dry-run)
id: sem
uses: tishlang/sem@v1
with:
dry_run: true
config: |
branches: [main]
plugins:
- "@sem/commit-analyzer"
- "@sem/release-notes-generator"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Skip when no bump
id: gate
run: |
if [ "${{ steps.sem.outputs.new_release_published }}" != "true" ]; then
echo "::notice::No version bump — skipping prerelease."
echo "continue=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "continue=true" >> "$GITHUB_OUTPUT"
echo "version=${{ steps.sem.outputs.new_release_version }}" >> "$GITHUB_OUTPUT"
echo "Next version: ${{ steps.sem.outputs.new_release_version }}"
# crates.io has no delete, only yank, and npm unpublish is a 72-hour window — so an artifact
# published under the wrong licence is effectively permanent. This repo has already shipped two
# versions under the wrong one and had to yank both. Refuse to build an artifact unless the
# LICENSE the packages and the crate point at is actually present.
- name: Refuse to package without a licence
if: steps.gate.outputs.continue == 'true'
run: |
for f in LICENSE packages/player/LICENSE packages/synths/LICENSE; do
if [ ! -f "$f" ]; then
echo "::error::Missing $f, but package.json declares a licence. Add the file, or correct the declaration, before anything is published."
exit 1
fi
done
- name: Stamp the version and pack @spacedevin/deck
if: steps.gate.outputs.continue == 'true'
run: |
node -e '
const fs = require("fs"), p = "./package.json";
const j = JSON.parse(fs.readFileSync(p, "utf8"));
j.version = process.argv[1];
fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n");
' "${{ steps.gate.outputs.version }}"
npm pack
mv spacedevin-deck-*.tgz spacedevin-deck-npm-package.tgz
# The synths catalog releases in lockstep too. Same `file:../..` → real version rewrite as the
# player, and for the same reason: the workspace links locally so CI builds the source in this
# repo, but a published tarball needs a version a consumer can actually resolve.
- name: Stamp the version and pack @spacedevin/deck-synths
if: steps.gate.outputs.continue == 'true'
run: |
node -e '
const fs = require("fs"), p = "./packages/synths/package.json";
const j = JSON.parse(fs.readFileSync(p, "utf8"));
j.version = process.argv[1];
j.peerDependencies["@spacedevin/deck"] = "^" + process.argv[1];
delete j.devDependencies["@spacedevin/deck"];
fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n");
' "${{ steps.gate.outputs.version }}"
npm pack -w @spacedevin/deck-synths
mv spacedevin-deck-synths-*.tgz spacedevin-deck-synths-npm-package.tgz
# The player releases in lockstep: same tag, same version. Its dependency on the language
# package is `file:../..` so the workspace links locally and CI tests the source in this repo —
# that has to become the real published version before packing, or the tarball is uninstallable.
#
# Caret, not an exact pin. `tish build` inlines the parser into dist/deck-player.js, so the
# dependency only matters to a consumer compiling from src/index.tish via the `tish` export
# condition — and for them a compatible minor is fine. An exact pin would just force npm to
# install a second copy alongside a consumer's own @spacedevin/deck.
- name: Stamp the version and pack @spacedevin/deck-player
if: steps.gate.outputs.continue == 'true'
run: |
node -e '
const fs = require("fs"), p = "./packages/player/package.json";
const j = JSON.parse(fs.readFileSync(p, "utf8"));
j.version = process.argv[1];
j.peerDependencies["@spacedevin/deck"] = "^" + process.argv[1];
delete j.devDependencies["@spacedevin/deck"];
fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n");
' "${{ steps.gate.outputs.version }}"
npm pack -w @spacedevin/deck-player
mv spacedevin-deck-player-*.tgz spacedevin-deck-player-npm-package.tgz
# Promoting this prerelease to a full release is what fires npm-release.yml and
# crates-release.yml. The two-step exists so a release is never published by the same run that
# decided to make one — there is a human between "CI is green" and "this is on npm forever".
- name: Create the prerelease via @sem/github
if: steps.gate.outputs.continue == 'true'
uses: tishlang/sem@v1
with:
force: ${{ steps.gate.outputs.version }}
config: |
branches: [main]
plugins:
- "@sem/commit-analyzer"
- "@sem/release-notes-generator"
- - "@sem/npm"
- npmPublish: false
- - "@sem/github"
- prerelease: true
successComment: false
assets:
- path: spacedevin-deck-npm-package.tgz
name: spacedevin-deck-npm-package.tgz
label: npm package (@spacedevin/deck)
- path: spacedevin-deck-player-npm-package.tgz
name: spacedevin-deck-player-npm-package.tgz
label: npm package (@spacedevin/deck-player)
- path: spacedevin-deck-synths-npm-package.tgz
name: spacedevin-deck-synths-npm-package.tgz
label: npm package (@spacedevin/deck-synths)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}