From 6b0df8bda99e3de2dd8b92a18bd59d7c21efd3fb Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 25 Sep 2026 22:47:02 -0400 Subject: [PATCH 1/6] ci: release-plz with binary builds; fix red main - release-plz.yml: release PR on every push to main; on merge it tags v, creates the GitHub release, then calls release.yml (6-target binaries + .deb + SHA256SUMS) and crates.yml from the same run, since a GITHUB_TOKEN tag cannot trigger the tag/release workflows. - cliff.toml: conventional-commit changelog, internal ticket ids scrubbed. - clippy (rust 1.97): sort_by_key, checked_div, collapsed match guard. - fix: `pxl install` treated every path dependency as stale when the project path contained a symlink (macOS /var); compare canonical paths. - browser-tests: drop the npm cache keyed on a lockfile that does not exist. - python: macos-13 runners are retired; use macos-15-intel. - README: crates badge pointed at an unrelated `pxl` crate; dead pixelsrc.dev link; Homebrew tap instruction pointed at a repo that does not exist; add `cargo install pixelsrc`. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PXBCFZMofE9EGKzo8J2YKJ --- .github/workflows/browser-tests.yml | 3 -- .github/workflows/crates.yml | 9 ++++ .github/workflows/python.yml | 2 +- .github/workflows/release-plz.yml | 59 +++++++++++++++++++++++ .github/workflows/release.yml | 16 ++++++- README.md | 14 ++---- cliff.toml | 72 +++++++++++++++++++++++++++++ release-plz.toml | 18 ++++++++ src/analyze/dimensions.rs | 2 +- src/analyze/families.rs | 2 +- src/analyze/tokens.rs | 4 +- src/atlas.rs | 2 +- src/composition/render.rs | 4 +- src/install.rs | 4 ++ src/lsp/server.rs | 10 ++-- 15 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/release-plz.yml create mode 100644 cliff.toml create mode 100644 release-plz.toml diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index be860870..9abe8e08 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -31,9 +31,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - cache-dependency-path: | - wasm/package-lock.json - name: Setup Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/crates.yml b/.github/workflows/crates.yml index f7d5d93b..3326aa1e 100644 --- a/.github/workflows/crates.yml +++ b/.github/workflows/crates.yml @@ -3,6 +3,13 @@ name: Publish to crates.io on: release: types: [published] + # Called by release-plz.yml after it tags a release (a GITHUB_TOKEN release + # cannot trigger the `release` arm above). + workflow_call: + inputs: + tag: + required: true + type: string workflow_dispatch: inputs: dry_run: @@ -23,6 +30,8 @@ jobs: id-token: write # Required for OIDC trusted publishing steps: - uses: actions/checkout@v4 + with: + ref: ${{ inputs.tag || github.ref }} - name: Setup Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 9fb5edc7..3e16edf3 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -41,7 +41,7 @@ jobs: fail-fast: false matrix: include: - - runner: macos-13 + - runner: macos-15-intel target: x86_64-apple-darwin - runner: macos-14 target: aarch64-apple-darwin diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml new file mode 100644 index 00000000..fb731d22 --- /dev/null +++ b/.github/workflows/release-plz.yml @@ -0,0 +1,59 @@ +name: Release-plz + +# On every push to main: open or update the release PR (version bump + +# CHANGELOG). When that PR merges: tag v, create the GitHub release, +# then build the binaries and publish to crates.io from this same run. +on: + push: + branches: [main] + +concurrency: + group: release-plz + cancel-in-progress: false + +jobs: + release-plz: + name: Release-plz + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + outputs: + releases_created: ${{ steps.release.outputs.releases_created }} + tag: ${{ steps.tag.outputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - uses: MarcoIeni/release-plz-action@v0.5 + id: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Resolve the new tag + id: tag + if: steps.release.outputs.releases_created == 'true' + env: + RELEASES: ${{ steps.release.outputs.releases }} + run: echo "tag=$(echo "$RELEASES" | jq -r '.[0].tag')" >> "$GITHUB_OUTPUT" + + binaries: + name: Binaries + needs: release-plz + if: needs.release-plz.outputs.releases_created == 'true' + permissions: + contents: write + uses: ./.github/workflows/release.yml + with: + tag: ${{ needs.release-plz.outputs.tag }} + + crates: + name: crates.io + needs: release-plz + if: needs.release-plz.outputs.releases_created == 'true' + permissions: + contents: read + id-token: write + uses: ./.github/workflows/crates.yml + with: + tag: ${{ needs.release-plz.outputs.tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b47eaedc..cf0dd804 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,13 @@ on: push: tags: - 'v*' + # Called by release-plz.yml after it tags a release (a GITHUB_TOKEN tag push + # cannot trigger the `tags` arm above). + workflow_call: + inputs: + tag: + required: true + type: string workflow_dispatch: inputs: version: @@ -13,7 +20,7 @@ on: env: CARGO_TERM_COLOR: always - VERSION: ${{ inputs.version || github.ref_name }} + VERSION: ${{ inputs.tag || inputs.version || github.ref_name }} jobs: build: @@ -49,6 +56,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ env.VERSION }} - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -97,6 +106,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ env.VERSION }} - name: Download Linux x86_64 binary uses: actions/download-artifact@v4 @@ -194,5 +205,6 @@ jobs: with: tag_name: ${{ env.VERSION }} files: artifacts/* - generate_release_notes: true + # release-plz already wrote the notes from the changelog; keep them. + generate_release_notes: ${{ inputs.tag == '' }} fail_on_unmatched_files: true diff --git a/README.md b/README.md index 229de4ed..2cff7e22 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ [![CI](https://github.com/scbrown/pixelsrc/actions/workflows/ci.yml/badge.svg)](https://github.com/scbrown/pixelsrc/actions/workflows/ci.yml) [![Release](https://github.com/scbrown/pixelsrc/actions/workflows/release.yml/badge.svg)](https://github.com/scbrown/pixelsrc/actions/workflows/release.yml) [![WASM](https://github.com/scbrown/pixelsrc/actions/workflows/wasm.yml/badge.svg)](https://github.com/scbrown/pixelsrc/actions/workflows/wasm.yml) -[![Version](https://img.shields.io/crates/v/pxl)](https://crates.io/crates/pxl) +[![Version](https://img.shields.io/crates/v/pixelsrc)](https://crates.io/crates/pixelsrc) [![npm](https://img.shields.io/npm/v/@stiwi/pixelsrc-wasm)](https://www.npmjs.com/package/@stiwi/pixelsrc-wasm) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Made with pixelsrc](https://img.shields.io/badge/made%20with-pixelsrc-BD93F9)](https://pixelsrc.dev) +[![Made with pixelsrc](https://img.shields.io/badge/made%20with-pixelsrc-BD93F9)](https://scbrown.github.io/pixelsrc/) **The first pixel art format designed for GenAI.** @@ -51,15 +51,11 @@ pixelsrc is a semantic, human-readable text format for defining pixel art. Unlik ## Installation -### Homebrew (macOS/Linux) - -```bash -brew install scbrown/tap/pixelsrc -``` - -### Cargo (from source) +### Cargo ```bash +cargo install pixelsrc +# or the latest main: cargo install --git https://github.com/scbrown/pixelsrc ``` diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..35fd6ffb --- /dev/null +++ b/cliff.toml @@ -0,0 +1,72 @@ +[changelog] +header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n" +footer = "" +body = """ +{%- macro remote_url() -%} + https://github.com/scbrown/pixelsrc +{%- endmacro -%} + +{% if version -%} +## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else -%} +## [Unreleased] +{% endif -%} + +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | striptags | trim | upper_first }} +{% for commit in commits %} +- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ +{% if commit.breaking %}[**breaking**] {% endif %}\ +{{ commit.message | upper_first }}\ +([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ +{% endfor %} +{% endfor %} +""" +# Preserve the body's trailing newline so adjacent release sections keep a +# blank line between the final list item and the next version heading. +trim = false + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +protect_breaking_commits = false +filter_commits = false +tag_pattern = "v[0-9].*" +sort_commits = "oldest" + +# Strip internal ticket IDs from PUBLISHED changelog entries. Commit messages +# keep the refs (internal git history wants them); the public CHANGELOG must +# not carry them — same enforcement split as the pre-push guard, and the same +# preprocessor block quipu's cliff.toml carries. Without this, an entry like +# "... pre-edit guard ()" ships verbatim (it was about to). +commit_preprocessors = [ + # Keep a leading tracker-style subject as a release entry. + { pattern = '^(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*:[ \t]*', replace = "other: " }, + # "(ID, rest)" -> "(rest)" e.g. (proj-1a2b, #36/#37) -> (#36/#37) + { pattern = '\((?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*,[ \t]*', replace = "(" }, + # "(rest, ID)" -> "(rest)" + { pattern = ',[ \t]*(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\)', replace = ")" }, + # " (ID)" standalone paren -> removed e.g. ... shape (proj-1a2b) -> ... shape + { pattern = '[ \t]*\((?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\)', replace = "" }, + # a RUN of bracketed refs -> removed e.g. ... dirs [proj-1a2b] [proj-3c4d] + { pattern = '(?:[ \t]*\[(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\])+', replace = "" }, + # "[ID, rest]" -> "[rest]" and "[rest, ID]" -> "[rest]", mirroring the parens + { pattern = '\[(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*,[ \t]*', replace = "[" }, + { pattern = ',[ \t]*(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\]', replace = "]" }, + # any bare leftover " ID" + { pattern = '[ \t]*\b(?:aegis|hq|gassy|qp|TTP)-[a-z0-9]{3,6}(?:\.[0-9]+)*\b', replace = "" }, +] + +commit_parsers = [ + { message = "^other", group = "Other" }, + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^refactor", group = "Changed" }, + { message = "^doc", group = "Documentation" }, + { message = "^test", group = "Testing" }, + { message = "^chore", group = "Miscellaneous" }, + { message = "^ci", group = "CI/CD" }, + { body = ".*security", group = "Security" }, + { message = "^revert", group = "Reverted" }, +] diff --git a/release-plz.toml b/release-plz.toml new file mode 100644 index 00000000..66648aef --- /dev/null +++ b/release-plz.toml @@ -0,0 +1,18 @@ +[workspace] +changelog_config = "cliff.toml" +# release-plz only cuts the release PR, the tag and the GitHub release. The +# binaries and the crates.io publish run as jobs in release-plz.yml, because a +# tag or release created with GITHUB_TOKEN does not trigger `on: push: tags` +# or `on: release` workflows. Left to those triggers, a release would ship +# with no binaries. +publish = false +# Use the tags as the baseline instead of asking crates.io; with publish off, +# the registry is not the source of truth. +git_only = true +git_release_enable = true +git_tag_enable = true +# The default template is `{{ package }}-v{{ version }}`; every tag here is v*. +git_tag_name = "v{{ version }}" + +[[package]] +name = "pixelsrc" diff --git a/src/analyze/dimensions.rs b/src/analyze/dimensions.rs index 447d1585..0ba1e45e 100644 --- a/src/analyze/dimensions.rs +++ b/src/analyze/dimensions.rs @@ -23,7 +23,7 @@ impl DimensionStats { /// Get dimension counts sorted by frequency. pub fn sorted_by_frequency(&self) -> Vec<((u32, u32), usize)> { let mut items: Vec<_> = self.dimensions.iter().map(|(k, v)| (*k, *v)).collect(); - items.sort_by(|a, b| b.1.cmp(&a.1)); + items.sort_by_key(|b| std::cmp::Reverse(b.1)); items } diff --git a/src/analyze/families.rs b/src/analyze/families.rs index 26af50a0..05bcf783 100644 --- a/src/analyze/families.rs +++ b/src/analyze/families.rs @@ -57,7 +57,7 @@ impl TokenFamilyDetector { .collect(); // Sort by total count descending - families.sort_by(|a, b| b.total_count.cmp(&a.total_count)); + families.sort_by_key(|f| std::cmp::Reverse(f.total_count)); families } diff --git a/src/analyze/tokens.rs b/src/analyze/tokens.rs index 2956e84c..3e84376d 100644 --- a/src/analyze/tokens.rs +++ b/src/analyze/tokens.rs @@ -111,7 +111,7 @@ impl CoOccurrenceMatrix { /// Get top N token pairs by co-occurrence count. pub fn top_n(&self, n: usize) -> Vec<((&String, &String), usize)> { let mut items: Vec<_> = self.pairs.iter().map(|((a, b), count)| ((a, b), *count)).collect(); - items.sort_by(|a, b| b.1.cmp(&a.1)); + items.sort_by_key(|b| std::cmp::Reverse(b.1)); items.truncate(n); items } @@ -131,7 +131,7 @@ impl CoOccurrenceMatrix { } }) .collect(); - results.sort_by(|a, b| b.1.cmp(&a.1)); + results.sort_by_key(|b| std::cmp::Reverse(b.1)); results } diff --git a/src/atlas.rs b/src/atlas.rs index cd20d616..c1ea00ed 100644 --- a/src/atlas.rs +++ b/src/atlas.rs @@ -129,7 +129,7 @@ pub fn pack_atlas( // Sort sprites by height descending (better packing) let mut sorted_sprites: Vec<(usize, &SpriteInput)> = sprites.iter().enumerate().collect(); - sorted_sprites.sort_by(|a, b| b.1.image.height().cmp(&a.1.image.height())); + sorted_sprites.sort_by_key(|s| std::cmp::Reverse(s.1.image.height())); let mut atlases: Vec<(RgbaImage, AtlasMetadata, Vec)> = vec![]; let mut sprite_to_atlas: HashMap = HashMap::new(); diff --git a/src/composition/render.rs b/src/composition/render.rs index 1f696bb8..f82223b7 100644 --- a/src/composition/render.rs +++ b/src/composition/render.rs @@ -147,8 +147,8 @@ pub fn render_composition( } // Calculate expected grid dimensions for map validation - let expected_cols = if cell_size[0] > 0 { width / cell_size[0] } else { width }; - let expected_rows = if cell_size[1] > 0 { height / cell_size[1] } else { height }; + let expected_cols = width.checked_div(cell_size[0]).unwrap_or(width); + let expected_rows = height.checked_div(cell_size[1]).unwrap_or(height); // Create canvas (transparent by default) let mut canvas = RgbaImage::from_pixel(width, height, Rgba([0, 0, 0, 0])); diff --git a/src/install.rs b/src/install.rs index 60bc9c83..937c8d84 100644 --- a/src/install.rs +++ b/src/install.rs @@ -111,6 +111,10 @@ impl LockFile { } else { project_root.join(declared_path) }; + // install_path_dep records the canonical path, so compare against that: + // otherwise any symlink in the project path (macOS /var -> /private/var) + // makes every path dep look stale and reinstall on each run. + let resolved = resolved.canonicalize().unwrap_or(resolved); let resolved_str = resolved.to_string_lossy().to_string(); locked.dep_type == "path" && locked.source == resolved_str } else if dep.is_git() { diff --git a/src/lsp/server.rs b/src/lsp/server.rs index fd192490..ccfeadb6 100644 --- a/src/lsp/server.rs +++ b/src/lsp/server.rs @@ -189,10 +189,12 @@ impl PixelsrcLanguageServer { // Likely inside a region definition return CompletionContext::RegionDef; } - Some("palette") if line.contains(':') => { - if content.contains("\"roles\"") && !line.contains("\"colors\"") { - return CompletionContext::Roles; - } + Some("palette") + if line.contains(':') + && content.contains("\"roles\"") + && !line.contains("\"colors\"") => + { + return CompletionContext::Roles; } _ => {} } From d134c9d08b85c772d179e727cb86981077ff3eb2 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 25 Sep 2026 22:51:17 -0400 Subject: [PATCH 2/6] ci: clippy late-init lint; drop native x86_64 linker override that broke wheel builds .cargo/config.toml forced x86_64-linux-gnu-gcc for the native Linux target, which does not exist in the manylinux container maturin builds in (and is just gcc on a plain x86_64 host). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PXBCFZMofE9EGKzo8J2YKJ --- .cargo/config.toml | 4 ---- src/build/pipeline.rs | 23 ++++++----------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 963acbaf..193fd7e2 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,9 +1,5 @@ # Cross-compilation configuration for pixelsrc/pxl -# Linux x86_64 (GNU) -[target.x86_64-unknown-linux-gnu] -linker = "x86_64-linux-gnu-gcc" - # Linux ARM64 (GNU) [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" diff --git a/src/build/pipeline.rs b/src/build/pipeline.rs index 5762b1e6..b0b5da0c 100644 --- a/src/build/pipeline.rs +++ b/src/build/pipeline.rs @@ -593,10 +593,7 @@ impl BuildPipeline { // Select registries with import support let has_file_imports = !file_imports.is_empty(); - let effective_palette_reg; - let effective_sprite_reg; - - if has_file_imports { + let (effective_palette_reg, effective_sprite_reg) = if has_file_imports { if let Some(pr) = project_registry { for palette_name in pr.palette_names() { if let Some(loc) = pr.palette_location(palette_name) { @@ -613,20 +610,12 @@ impl BuildPipeline { local_sprite_registry.register_sprite(sprite.clone()); } } - effective_palette_reg = local_palette_registry; - effective_sprite_reg = local_sprite_registry; + (local_palette_registry, local_sprite_registry) + } else if let Some(pr) = project_registry { + (pr.palettes.clone(), pr.sprites.clone()) } else { - effective_palette_reg = if let Some(pr) = project_registry { - pr.palettes.clone() - } else { - local_palette_registry - }; - effective_sprite_reg = if let Some(pr) = project_registry { - pr.sprites.clone() - } else { - local_sprite_registry - }; - } + (local_palette_registry, local_sprite_registry) + }; let palette_registry = &effective_palette_reg; let sprite_registry = &effective_sprite_reg; From 701470e5659e32d9045cb48d6ba736bff35c5881 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 25 Sep 2026 23:03:00 -0400 Subject: [PATCH 3/6] ci: unbreak the Python wheel and browser-test lanes - drop .cargo/config.toml: its linker overrides (x86_64-linux-gnu-gcc, aarch64-linux-gnu-gcc) do not exist in maturin's manylinux containers, and cross sets its own linker anyway; the macOS entries were defaults. - wheels: pyo3 0.23 rejects the 3.14 interpreter that --find-interpreter now finds; build it against the stable ABI. - browser tests: npx serve was still downloading when the 3s wait ended (ERR_EMPTY_RESPONSE); serve the book with python3 -m http.server. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PXBCFZMofE9EGKzo8J2YKJ --- .cargo/config.toml | 17 ----------------- .github/workflows/browser-tests.yml | 5 ++--- .github/workflows/python.yml | 3 +++ 3 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 193fd7e2..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,17 +0,0 @@ -# Cross-compilation configuration for pixelsrc/pxl - -# Linux ARM64 (GNU) -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" - -# macOS x86_64 -[target.x86_64-apple-darwin] -linker = "clang" - -# macOS ARM64 (Apple Silicon) -[target.aarch64-apple-darwin] -linker = "clang" - -# Windows x86_64 (MSVC) -[target.x86_64-pc-windows-msvc] -# Uses default MSVC linker when available diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index 9abe8e08..142f17c9 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -74,9 +74,8 @@ jobs: test.beforeAll(async () => { // Start a simple HTTP server - server = spawn('npx', ['serve', '-p', PORT, 'docs/book/book'], { - stdio: 'pipe', - shell: true + server = spawn('python3', ['-m', 'http.server', String(PORT), '--directory', 'docs/book/book'], { + stdio: 'pipe' }); // Wait for server to start await new Promise(resolve => setTimeout(resolve, 3000)); diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 3e16edf3..ce8d25a4 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -10,6 +10,9 @@ on: env: CARGO_TERM_COLOR: always + # pyo3 0.23 tops out at Python 3.13; runners now carry 3.14, which + # --find-interpreter picks up. Build those against the stable ABI. + PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1" jobs: linux: From e168b994565f0679b6b249a5336d371284495ccd Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 25 Sep 2026 23:09:21 -0400 Subject: [PATCH 4/6] ci: publish the Homebrew formula to the scbrown/homebrew-pixelsrc tap The formula lived in homebrew-pixelsrc/ inside this repo, which brew cannot tap, and the README pointed at scbrown/tap, which never existed. The tap is now its own repo (seeded with 0.2.0, verified with brew install + brew test). homebrew.yml checks it out with a write deploy key and pushes the regenerated formula; release-plz.yml calls it after the binaries are attached. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PXBCFZMofE9EGKzo8J2YKJ --- .github/workflows/homebrew.yml | 36 ++++++++++++++----- .github/workflows/release-plz.yml | 12 ++++++- README.md | 6 ++++ homebrew-pixelsrc/Formula/pxl-src.rb | 23 ------------ homebrew-pixelsrc/Formula/pxl.rb | 41 ---------------------- homebrew-pixelsrc/README.md | 52 ---------------------------- 6 files changed, 45 insertions(+), 125 deletions(-) delete mode 100644 homebrew-pixelsrc/Formula/pxl-src.rb delete mode 100644 homebrew-pixelsrc/Formula/pxl.rb delete mode 100644 homebrew-pixelsrc/README.md diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index 7b389c8d..e19008b5 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -3,6 +3,16 @@ name: Update Homebrew Formula on: release: types: [published] + # Called by release-plz.yml once the binaries are attached (a GITHUB_TOKEN + # release cannot trigger the `release` arm above). + workflow_call: + inputs: + tag: + required: true + type: string + secrets: + HOMEBREW_TAP_DEPLOY_KEY: + required: true workflow_dispatch: inputs: version: @@ -10,25 +20,34 @@ on: required: true type: string +# The formula lives in the tap repo, scbrown/homebrew-pixelsrc, so that +# `brew install scbrown/pixelsrc/pxl` works. CI writes to it with a deploy key +# (HOMEBREW_TAP_DEPLOY_KEY) scoped to that one repo. env: - FORMULA_FILE: homebrew-pixelsrc/Formula/pxl.rb + FORMULA_FILE: tap/Formula/pxl.rb + # the working directory is not a pixelsrc checkout, so tell gh which repo + GH_REPO: ${{ github.repository }} jobs: update-formula: name: Update Homebrew formula runs-on: ubuntu-latest permissions: - contents: write + contents: read steps: - - name: Checkout + - name: Checkout the tap uses: actions/checkout@v4 with: - ref: main + repository: scbrown/homebrew-pixelsrc + ssh-key: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} + path: tap - name: Get version id: version run: | - if [ -n "${{ inputs.version }}" ]; then + if [ -n "${{ inputs.tag }}" ]; then + VERSION="${{ inputs.tag }}" + elif [ -n "${{ inputs.version }}" ]; then VERSION="${{ inputs.version }}" else VERSION="${{ github.event.release.tag_name }}" @@ -88,7 +107,7 @@ jobs: # frozen_string_literal: true # Homebrew formula for pxl - Pixelsrc CLI - # Install: brew tap scbrown/pixelsrc && brew install pxl + # Install: brew install scbrown/pixelsrc/pxl class Pxl < Formula desc "GenAI-native pixel art format and compiler" homepage "https://github.com/scbrown/pixelsrc" @@ -131,9 +150,10 @@ jobs: cat $FORMULA_FILE - name: Commit and push + working-directory: tap run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add $FORMULA_FILE - git diff --staged --quiet || git commit -m "Update Homebrew formula for ${{ steps.version.outputs.tag }}" + git add Formula/pxl.rb + git diff --staged --quiet || git commit -m "pxl ${{ steps.version.outputs.number }}" git push diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index fb731d22..fae2a395 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -2,7 +2,8 @@ name: Release-plz # On every push to main: open or update the release PR (version bump + # CHANGELOG). When that PR merges: tag v, create the GitHub release, -# then build the binaries and publish to crates.io from this same run. +# then build the binaries, update the Homebrew tap and publish to crates.io +# from this same run. on: push: branches: [main] @@ -47,6 +48,15 @@ jobs: with: tag: ${{ needs.release-plz.outputs.tag }} + homebrew: + name: Homebrew tap + needs: [release-plz, binaries] + uses: ./.github/workflows/homebrew.yml + with: + tag: ${{ needs.release-plz.outputs.tag }} + secrets: + HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} + crates: name: crates.io needs: release-plz diff --git a/README.md b/README.md index 2cff7e22..02d450e9 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ pixelsrc is a semantic, human-readable text format for defining pixel art. Unlik ## Installation +### Homebrew (macOS/Linux) + +```bash +brew install scbrown/pixelsrc/pxl +``` + ### Cargo ```bash diff --git a/homebrew-pixelsrc/Formula/pxl-src.rb b/homebrew-pixelsrc/Formula/pxl-src.rb deleted file mode 100644 index a0de66f6..00000000 --- a/homebrew-pixelsrc/Formula/pxl-src.rb +++ /dev/null @@ -1,23 +0,0 @@ -# typed: false -# frozen_string_literal: true - -# Homebrew formula for pxl - build from source -# Install: brew tap scbrown/pixelsrc && brew install pxl-src -class PxlSrc < Formula - desc "GenAI-native pixel art format and compiler (build from source)" - homepage "https://github.com/scbrown/pixelsrc" - url "https://github.com/scbrown/pixelsrc/archive/refs/tags/v0.1.0.tar.gz" - sha256 "PLACEHOLDER_SHA256_SOURCE" - license "MIT" - head "https://github.com/scbrown/pixelsrc.git", branch: "main" - - depends_on "rust" => :build - - def install - system "cargo", "install", *std_cargo_args - end - - test do - assert_match version.to_s, shell_output("#{bin}/pxl --version") - end -end diff --git a/homebrew-pixelsrc/Formula/pxl.rb b/homebrew-pixelsrc/Formula/pxl.rb deleted file mode 100644 index 725e1826..00000000 --- a/homebrew-pixelsrc/Formula/pxl.rb +++ /dev/null @@ -1,41 +0,0 @@ -# typed: false -# frozen_string_literal: true - -# Homebrew formula for pxl - Pixelsrc CLI -# Install: brew tap scbrown/pixelsrc && brew install pxl -class Pxl < Formula - desc "GenAI-native pixel art format and compiler" - homepage "https://github.com/scbrown/pixelsrc" - version "0.2.0" - license "MIT" - - on_macos do - on_arm do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-aarch64-apple-darwin.tar.gz" - sha256 "243cb8a3fddb10ae73c76e9a223ee91fab350d1c76e2e1a6a53d73dd8c5e153b" - end - on_intel do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-x86_64-apple-darwin.tar.gz" - sha256 "d6f6e559717337b11585a6193f28b2d12afdae46a377f4d0e0385314a053f880" - end - end - - on_linux do - on_arm do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-aarch64-unknown-linux-gnu.tar.gz" - sha256 "102572e116fae5e5fcdb2bb75c964eda1d46a66e8304bf34a45d4c3b412cc4d2" - end - on_intel do - url "https://github.com/scbrown/pixelsrc/releases/download/v0.2.0/pxl-v0.2.0-x86_64-unknown-linux-gnu.tar.gz" - sha256 "d376a081598379b105d11a36ef531eaedbb788c447311a643f4a1c190e032893" - end - end - - def install - bin.install "pxl" - end - - test do - assert_match version.to_s, shell_output("#{bin}/pxl --version") - end -end diff --git a/homebrew-pixelsrc/README.md b/homebrew-pixelsrc/README.md deleted file mode 100644 index 0b7fe4df..00000000 --- a/homebrew-pixelsrc/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# Homebrew Tap for Pixelsrc - -This tap contains the Homebrew formula for `pxl`, the Pixelsrc CLI. - -## Installation - -```bash -# Add the tap -brew tap stiwi/pixelsrc - -# Install pxl -brew install pxl -``` - -## Available Formulas - -### pxl (binary) - -Pre-built binary for fast installation: - -```bash -brew install pxl -``` - -Supports: -- macOS (Intel and Apple Silicon) -- Linux (x86_64 and ARM64) - -### pxl-src (build from source) - -Build from source (requires Rust): - -```bash -brew install pxl-src -``` - -## Updating - -```bash -brew update -brew upgrade pxl -``` - -## Verification - -```bash -pxl --version -``` - -## About Pixelsrc - -Pixelsrc is a GenAI-native pixel art format and compiler. See the main repository for documentation. From 52826c768186c73a224f1d348fb802002fb2eed6 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 25 Sep 2026 23:28:44 -0400 Subject: [PATCH 5/6] ci: browser tests serve mdBook's html/ output; Linux wheels find interpreters With [output.linkcheck] configured, mdBook writes the site to book/html/, so the tests were loading a bare directory listing. The aarch64 manylinux-cross container has no default interpreter, so maturin needs --find-interpreter like the macOS and Windows jobs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PXBCFZMofE9EGKzo8J2YKJ --- .github/workflows/browser-tests.yml | 2 +- .github/workflows/python.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index 142f17c9..f2cbbbe2 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -74,7 +74,7 @@ jobs: test.beforeAll(async () => { // Start a simple HTTP server - server = spawn('python3', ['-m', 'http.server', String(PORT), '--directory', 'docs/book/book'], { + server = spawn('python3', ['-m', 'http.server', String(PORT), '--directory', 'docs/book/book/html'], { stdio: 'pipe' }); // Wait for server to start diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index ce8d25a4..c953d66f 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -28,7 +28,7 @@ jobs: - uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist + args: --release --out dist --find-interpreter manylinux: auto working-directory: python From 34259d06c288acc75c969a6e057e813a43796835 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Fri, 25 Sep 2026 23:59:02 -0400 Subject: [PATCH 6/6] fix(docs): interactive demos never loaded; wheel and browser-test lanes green - wasm-demo.js probed dynamic import with `typeof import`, a SyntaxError that aborted the whole script, so window.pixelsrcDemo was never defined and every interactive example in the book was dead. - browser test: open mdBook search before asserting the bar is visible. - Linux wheels: the pyo3 forward-compat env var does not reach the manylinux container; list the supported interpreters explicitly. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PXBCFZMofE9EGKzo8J2YKJ --- .github/workflows/browser-tests.yml | 2 ++ .github/workflows/python.yml | 4 +++- docs/book/custom/js/wasm-demo.js | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml index f2cbbbe2..024d01dd 100644 --- a/.github/workflows/browser-tests.yml +++ b/.github/workflows/browser-tests.yml @@ -102,6 +102,8 @@ jobs: test('search functionality works', async ({ page }) => { await page.goto(BASE_URL); // Check search is available + // mdBook keeps the search bar hidden until search is opened + await page.locator('#search-toggle').click(); const searchInput = page.locator('#searchbar'); await expect(searchInput).toBeVisible(); }); diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c953d66f..3a6b113d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -28,7 +28,9 @@ jobs: - uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist --find-interpreter + # PYO3_USE_ABI3_FORWARD_COMPATIBILITY does not reach the manylinux + # container, so name the interpreters pyo3 0.23 supports. + args: --release --out dist -i python3.9 python3.10 python3.11 python3.12 python3.13 manylinux: auto working-directory: python diff --git a/docs/book/custom/js/wasm-demo.js b/docs/book/custom/js/wasm-demo.js index 48ec628d..238acb01 100644 --- a/docs/book/custom/js/wasm-demo.js +++ b/docs/book/custom/js/wasm-demo.js @@ -18,8 +18,10 @@ wasm: typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function', dynamicImport: (function() { try { - // Check if dynamic import is supported - return typeof import === 'function' || true; // Modern browsers + // Check if dynamic import is supported. `typeof import` is a + // SyntaxError that aborts this whole script, so probe via Function. + new Function('return import("data:text/javascript,")'); + return true; } catch (e) { return false; }