fix(ci): drop the dry-run package dir before the cargo cache is saved #40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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. | |
| 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 | |
| release_check: | |
| name: Release check (semantic-release dry-run) | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: Install dependencies (incl. semantic-release) | |
| run: npm ci | |
| - name: Set repository URL | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const p = require('./package.json'); | |
| p.repository = { type: 'git', url: process.env.REPO_URL }; | |
| fs.writeFileSync('./package.json', JSON.stringify(p, null, 2)); | |
| " | |
| env: | |
| REPO_URL: ${{ format('{0}/{1}.git', github.server_url, github.repository) }} | |
| - name: Require incremental release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| npx semantic-release --dry-run --extends ./.releaserc.json 2>&1 | tee release-check.log | |
| if grep -q "There are no relevant changes, so no new version is released." release-check.log; then | |
| echo "No incremental release would be triggered. Push commits that trigger a release (feat/fix/perf/BREAKING CHANGE per conventional commits)." | |
| exit 1 | |
| fi | |
| echo "Release would be triggered - check passed." | |
| release: | |
| name: Release (prerelease branch + GitHub API) | |
| needs: [test, rust_crate, release_check] | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Set repository URL | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const p = require('./package.json'); | |
| p.repository = { type: 'git', url: process.env.REPO_URL }; | |
| fs.writeFileSync('./package.json', JSON.stringify(p, null, 2)); | |
| " | |
| env: | |
| REPO_URL: ${{ format('{0}/{1}.git', github.server_url, github.repository) }} | |
| - name: Get next version (semantic-release dry-run) | |
| id: next_version | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| npx semantic-release --dry-run --extends ./.releaserc.json 2>&1 | tee release.log | |
| VERSION=$(sed -n 's/.*Published release \([0-9.]*\).*/\1/p' release.log | head -1) | |
| if [ -z "$VERSION" ]; then | |
| echo "Could not determine next version from semantic-release output" | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Next version: $VERSION" | |
| - name: Set package version | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const p = require('./package.json'); | |
| p.version = process.env.VERSION; | |
| fs.writeFileSync('./package.json', JSON.stringify(p, null, 2)); | |
| " | |
| env: | |
| VERSION: ${{ steps.next_version.outputs.version }} | |
| - name: Create npm package tarball (@spacedevin/deck) | |
| run: | | |
| npm pack | |
| mv spacedevin-deck-*.tgz spacedevin-deck-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: Set player version and pin its dependency | |
| run: | | |
| node -e " | |
| const fs = require('fs'); | |
| const p = './packages/player/package.json'; | |
| const j = JSON.parse(fs.readFileSync(p)); | |
| j.version = process.env.VERSION; | |
| j.dependencies['@spacedevin/deck'] = '^' + process.env.VERSION; | |
| fs.writeFileSync(p, JSON.stringify(j, null, 2) + '\n'); | |
| " | |
| env: | |
| VERSION: ${{ steps.next_version.outputs.version }} | |
| - name: Create npm package tarball (@spacedevin/deck-player) | |
| run: | | |
| npm pack -w @spacedevin/deck-player | |
| mv spacedevin-deck-player-*.tgz spacedevin-deck-player-npm-package.tgz | |
| - name: Create or update release branch and push | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| BRANCH="release/v${{ steps.next_version.outputs.version }}" | |
| git checkout -B "$BRANCH" | |
| git push origin "$BRANCH" --force | |
| - name: Generate release notes from commits | |
| id: changelog | |
| run: | | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -n "$LAST_TAG" ]; then | |
| CHANGELOG=$(git log "$LAST_TAG..HEAD" --pretty=format:"- %s (%h)" --no-merges 2>/dev/null || echo "") | |
| else | |
| CHANGELOG=$(git log -30 --pretty=format:"- %s (%h)" --no-merges 2>/dev/null || echo "") | |
| fi | |
| [ -z "$CHANGELOG" ] && CHANGELOG="- No commits to list" | |
| { | |
| echo "## Changes" | |
| echo "" | |
| echo "$CHANGELOG" | |
| echo "" | |
| } > release-body.md | |
| - name: Create or update GitHub prerelease via API | |
| id: create_release | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ steps.next_version.outputs.version }} | |
| run: | | |
| TAG="v${VERSION}" | |
| BRANCH="release/v${VERSION}" | |
| BODY=$(cat release-body.md) | |
| RESP=$(curl -s -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases" \ | |
| -d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"$BRANCH\",\"name\":\"$TAG\",\"body\":$(echo "$BODY" | jq -Rs .),\"prerelease\":true}") | |
| HTTP_CODE=$(echo "$RESP" | tail -n1) | |
| BODY_RESP=$(echo "$RESP" | sed '$d') | |
| if [ "$HTTP_CODE" = "201" ]; then | |
| echo "Created prerelease $TAG" | |
| RELEASE_ID=$(echo "$BODY_RESP" | jq -r .id) | |
| elif [ "$HTTP_CODE" = "422" ]; then | |
| RELEASE_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG" | jq -r .id) | |
| if [ "$RELEASE_ID" != "null" ] && [ -n "$RELEASE_ID" ]; then | |
| curl -s -X PATCH \ | |
| -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID" \ | |
| -d "{\"target_commitish\":\"$BRANCH\",\"body\":$(echo "$BODY" | jq -Rs .),\"prerelease\":true}" | |
| echo "Updated existing prerelease $TAG" | |
| else | |
| echo "Release creation failed (422) and could not find release by tag" | |
| exit 1 | |
| fi | |
| else | |
| echo "Unexpected response: $HTTP_CODE" | |
| echo "$BODY_RESP" | |
| exit 1 | |
| fi | |
| echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT | |
| - name: Upload npm package to prerelease | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| RELEASE_ID="${{ steps.create_release.outputs.release_id }}" | |
| REPO="${{ github.repository }}" | |
| UPLOAD_URL="https://uploads.github.com/repos/$REPO/releases/$RELEASE_ID/assets" | |
| curl -s -X POST \ | |
| -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Content-Type: application/octet-stream" \ | |
| --data-binary @spacedevin-deck-npm-package.tgz \ | |
| "${UPLOAD_URL}?name=spacedevin-deck-npm-package.tgz&label=npm%20package%20(@spacedevin/deck)" | |
| curl -s -X POST \ | |
| -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -H "Content-Type: application/octet-stream" \ | |
| --data-binary @spacedevin-deck-player-npm-package.tgz \ | |
| "${UPLOAD_URL}?name=spacedevin-deck-player-npm-package.tgz&label=npm%20package%20(@spacedevin/deck-player)" |