diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml new file mode 100644 index 0000000..f85087f --- /dev/null +++ b/.github/workflows/upstream.yml @@ -0,0 +1,240 @@ +name: Upstream + +concurrency: + group: upstream + +on: + schedule: + - cron: "0 */6 * * *" + workflow_dispatch: + inputs: + lightningcss_version: + description: "Lightning CSS release to bump to (e.g. 1.33.0). Leave blank to auto-detect the latest release on GitHub." + required: false + type: string + gem_version: + description: "Gem version to release. Leave blank to match the Lightning CSS release. Use a fourth digit (e.g. 1.33.0.1) to re-release the same version." + required: false + type: string + +permissions: + contents: read + +env: + UPSTREAM_REPOSITORY: parcel-bundler/lightningcss + +jobs: + bump: + name: Detect and PR a new Lightning CSS release + runs-on: ubuntu-latest + timeout-minutes: 90 + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v6 + with: + ref: main + persist-credentials: false + + - name: Determine Lightning CSS version + id: upstream + env: + GH_TOKEN: ${{ github.token }} + INPUT_VERSION: ${{ inputs.lightningcss_version }} + INPUT_GEM_VERSION: ${{ inputs.gem_version }} + run: | + set -euo pipefail + + if [[ -n "${INPUT_VERSION}" ]]; then + upstream_version="${INPUT_VERSION#v}" + else + upstream_version=$(gh api "repos/${UPSTREAM_REPOSITORY}/releases/latest" --jq '.tag_name') + upstream_version="${upstream_version#v}" + fi + + if [[ ! "${upstream_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::${upstream_version} is not a Lightning CSS release of the form X.Y.Z" + exit 1 + fi + + crate_version=$(gh api "repos/${UPSTREAM_REPOSITORY}/contents/Cargo.toml?ref=v${upstream_version}" \ + --jq '.content' | base64 -d | awk '/^\[package\]/{p=1} p && /^version = /{gsub(/version = |"/, ""); print; exit}') + + if [[ -z "${crate_version}" ]]; then + echo "::error::could not read the crate version from Cargo.toml at v${upstream_version}" + exit 1 + fi + + if ! curl -sSf "https://crates.io/api/v1/crates/lightningcss/${crate_version}" \ + -H "User-Agent: lightningcss-ruby upstream workflow" > /dev/null; then + echo "::error::lightningcss ${crate_version} is not published on crates.io" + exit 1 + fi + + gem_version="${INPUT_GEM_VERSION:-${upstream_version}}" + + if [[ ! "${gem_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo "::error::gem version ${gem_version} is not of the form X.Y.Z or X.Y.Z.N" + exit 1 + fi + + current_crate_version=$(grep -E '^lightningcss = \{ version = "=' rust/Cargo.toml | sed -E 's/.*version = "=([^"]+)".*/\1/') + current_gem_version=$(sed -n -E 's/^ VERSION = "(.*)"$/\1/p' lib/lightningcss/version.rb) + + echo "upstream_version=${upstream_version}" >> "$GITHUB_OUTPUT" + echo "crate_version=${crate_version}" >> "$GITHUB_OUTPUT" + echo "current_crate_version=${current_crate_version}" >> "$GITHUB_OUTPUT" + echo "gem_version=${gem_version}" >> "$GITHUB_OUTPUT" + echo "current_gem_version=${current_gem_version}" >> "$GITHUB_OUTPUT" + + if [[ "${crate_version}" == "${current_crate_version}" && "${gem_version}" == "${current_gem_version}" ]]; then + echo "Already up to date (Lightning CSS ${upstream_version}, crate ${current_crate_version}, gem ${current_gem_version})" + echo "needed=false" >> "$GITHUB_OUTPUT" + else + echo "New release: Lightning CSS v${upstream_version} carries crate ${crate_version}" + echo "crate ${current_crate_version} -> ${crate_version}, gem ${current_gem_version} -> ${gem_version}" + echo "needed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Check for an existing PR + if: steps.upstream.outputs.needed == 'true' + id: existing_pr + env: + GH_TOKEN: ${{ github.token }} + GEM_VERSION: ${{ steps.upstream.outputs.gem_version }} + run: | + set -euo pipefail + + pr=$(gh pr list --head "bot-dep-lightningcss-${GEM_VERSION}" --json number --jq '.[0].number // empty') + + if [[ -n "${pr}" ]]; then + echo "PR #${pr} already exists for ${GEM_VERSION}" + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Set up Rust + if: steps.upstream.outputs.needed == 'true' && steps.existing_pr.outputs.exists == 'false' + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache-workspaces: rust + + - name: Set up Ruby + if: steps.upstream.outputs.needed == 'true' && steps.existing_pr.outputs.exists == 'false' + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Bump Lightning CSS and the gem version + if: steps.upstream.outputs.needed == 'true' && steps.existing_pr.outputs.exists == 'false' + env: + CRATE_VERSION: ${{ steps.upstream.outputs.crate_version }} + GEM_VERSION: ${{ steps.upstream.outputs.gem_version }} + run: | + set -euo pipefail + + sed -E -i "s|^(lightningcss = \{ version = \"=)[^\"]*(\")|\1${CRATE_VERSION}\2|" rust/Cargo.toml + sed -E -i "s|^ VERSION = \".*\"| VERSION = \"${GEM_VERSION}\"|" lib/lightningcss/version.rb + + sed -E -i "s|assert_equal \".*\", LightningCSS::VERSION|assert_equal \"${GEM_VERSION}\", LightningCSS::VERSION|" test/lightningcss_test.rb + sed -E -i "s|assert_equal \".*\", LightningCSS\.lightningcss_version|assert_equal \"${CRATE_VERSION}\", LightningCSS.lightningcss_version|" test/lightningcss_test.rb + + git diff --stat + + - name: Update the lockfiles + if: steps.upstream.outputs.needed == 'true' && steps.existing_pr.outputs.exists == 'false' + env: + CRATE_VERSION: ${{ steps.upstream.outputs.crate_version }} + run: | + set -euo pipefail + + cargo fetch --manifest-path rust/Cargo.toml + + bundle config set --local deployment false + bundle config set --local frozen false + bundle install + + locked=$(sed -n -E '/^name = "lightningcss"$/{n;s/^version = "(.*)"$/\1/p;}' rust/Cargo.lock) + + if [[ "${locked}" != "${CRATE_VERSION}" ]]; then + echo "::error::rust/Cargo.lock pins lightningcss ${locked}, expected ${CRATE_VERSION}" + exit 1 + fi + + - name: Build and test against the new version + id: verify + if: steps.upstream.outputs.needed == 'true' && steps.existing_pr.outputs.exists == 'false' + continue-on-error: true + run: | + set -o pipefail + + bundle exec rake test 2>&1 | tee "${RUNNER_TEMP}/verify.log" + + - name: Create pull request + if: steps.upstream.outputs.needed == 'true' && steps.existing_pr.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.LIGHTNINGCSS_RUBY_PR_TOKEN || github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + UPSTREAM_VERSION: ${{ steps.upstream.outputs.upstream_version }} + CRATE_VERSION: ${{ steps.upstream.outputs.crate_version }} + CURRENT_CRATE_VERSION: ${{ steps.upstream.outputs.current_crate_version }} + CURRENT_GEM_VERSION: ${{ steps.upstream.outputs.current_gem_version }} + GEM_VERSION: ${{ steps.upstream.outputs.gem_version }} + VERIFIED: ${{ steps.verify.outcome }} + run: | + set -euo pipefail + + branch="bot-dep-lightningcss-${GEM_VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + + git switch -c "${branch}" + git add rust/Cargo.toml rust/Cargo.lock lib/lightningcss/version.rb test/lightningcss_test.rb Gemfile.lock + git commit -m "$(printf "Bump Lightning CSS to %s\n\nUpdate the embedded Lightning CSS dependency from %s to %s and release it\nas gem v%s.\n\nhttps://github.com/%s/releases/tag/v%s" \ + "${UPSTREAM_VERSION}" "${CURRENT_CRATE_VERSION}" "${CRATE_VERSION}" "${GEM_VERSION}" "${UPSTREAM_REPOSITORY}" "${UPSTREAM_VERSION}")" + + git push --force -u origin "${branch}" + + body="${RUNNER_TEMP}/pr-body.md" + + { + printf 'Update the embedded Lightning CSS dependency to [v%s](https://github.com/%s/releases/tag/v%s), which carries crate `%s` (was `%s`).\n\n' \ + "${UPSTREAM_VERSION}" "${UPSTREAM_REPOSITORY}" "${UPSTREAM_VERSION}" "${CRATE_VERSION}" "${CURRENT_CRATE_VERSION}" + printf 'The gem version tracks the Lightning CSS release it packages, so this also bumps the gem from %s to **%s**.\n\n' \ + "${CURRENT_GEM_VERSION}" "${GEM_VERSION}" + } > "${body}" + + if [[ "${VERIFIED}" == "success" ]]; then + printf 'The gem was built and tested against the new version before this PR was opened. Merging this does not publish anything, cut a `v%s` release to push the gems.\n' \ + "${GEM_VERSION}" >> "${body}" + else + { + printf '> [!WARNING]\n' + printf '> The gem did not build or test cleanly against this version, so this PR is a draft.\n' + printf '> Something upstream changed in a way this gem has to be adapted to.\n\n' + printf '
Verification output\n\n```\n' + sed -E 's/\x1b\[[0-9;]*[A-Za-z]//g' "${RUNNER_TEMP}/verify.log" | tail -c 8000 + printf '\n```\n\n
\n' + } >> "${body}" + fi + + printf '\nThis PR was created automatically by the upstream workflow.\n' >> "${body}" + + if [[ "${VERIFIED}" == "success" ]]; then + gh pr create --title "Bump Lightning CSS to ${UPSTREAM_VERSION}" --body-file "${body}" + else + gh pr create --draft --title "Bump Lightning CSS to ${UPSTREAM_VERSION}" --body-file "${body}" + fi + + - name: Report the verification result + if: steps.upstream.outputs.needed == 'true' && steps.existing_pr.outputs.exists == 'false' && steps.verify.outcome == 'failure' + run: | + echo "::error::The gem did not build or test cleanly against the new version. A draft PR was opened to fix it in." + exit 1 diff --git a/README.md b/README.md index 88d17fd..71b36b3 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,16 @@ bundle add lightningcss Precompiled gems are published for Linux (gnu and musl) and macOS, on x86_64, aarch64, and arm. On those platforms nothing is compiled at install time. Anywhere else the gem builds from source and needs the [Rust toolchain](https://rustup.rs). +### Versioning + +The gem carries the version of the Lightning CSS release it packages. Gem v1.33.0 bundles [Lightning CSS v1.33.0](https://github.com/parcel-bundler/lightningcss/releases/tag/v1.33.0). + +Lightning CSS publishes its Rust crate under a different number than it releases under: v1.33.0 ships the crate as `1.0.0-alpha.72`. The gem follows the release, since that is the version everything else calls it. `LightningCSS.lightningcss_version` reports the crate version a given build was compiled against. + +So the version tracks Lightning CSS, not the Ruby API here. A minor bump is a minor Lightning CSS release, and says nothing about whether this gem's own API moved. Pin on the Lightning CSS version you want. + +If the gem needs releasing again for the same Lightning CSS version, that release adds a fourth digit, as in v1.33.0.1. + ### Usage #### Transforming diff --git a/rust/Cargo.lock b/rust/Cargo.lock index fff3cc4..a8eeebb 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -527,7 +527,7 @@ dependencies = [ [[package]] name = "lightningcss-ffi" -version = "0.2.0" +version = "0.0.0" dependencies = [ "cbindgen", "lightningcss", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f29643b..619a11e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,7 +2,6 @@ [package] name = "lightningcss-ffi" -version = "0.2.0" edition = "2021" authors = ["Marco Roth "] description = "C FFI bindings for Lightning CSS, used by the lightningcss gem" @@ -16,7 +15,7 @@ path = "src/lib.rs" crate-type = ["cdylib", "staticlib", "rlib"] [dependencies] -lightningcss = { version = "1.0.0-alpha.68", features = ["visitor"] } +lightningcss = { version = "=1.0.0-alpha.72", features = ["visitor"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/rust/build.rs b/rust/build.rs index f6616eb..fe43502 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -28,10 +28,32 @@ fn main() { PathBuf::from(&crate_dir).join("cbindgen.toml").display() ); + let version_rb_path = PathBuf::from(&crate_dir).join("../lib/lightningcss/version.rb"); + + println!("cargo:rerun-if-changed={}", version_rb_path.display()); + println!( "cargo:rustc-env=LIGHTNINGCSS_VERSION={}", locked_version(&lock_path, "lightningcss") ); + + println!("cargo:rustc-env=GEM_VERSION={}", gem_version(&version_rb_path)); +} + +fn gem_version(version_rb_path: &PathBuf) -> String { + let Ok(source) = fs::read_to_string(version_rb_path) else { + return "unknown".to_string(); + }; + + for line in source.lines() { + let Some(rest) = line.trim().strip_prefix("VERSION = ") else { + continue; + }; + + return rest.trim().trim_matches('"').to_string(); + } + + "unknown".to_string() } fn locked_version(lock_path: &PathBuf, package: &str) -> String { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 2c7a110..cfd002b 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -24,7 +24,7 @@ use lightningcss::visitor::Visit; use crate::options::{TransformOptions, TransformResult}; use crate::scope::Scoper; -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const VERSION: &str = env!("GEM_VERSION"); pub const LIGHTNINGCSS_VERSION: &str = env!("LIGHTNINGCSS_VERSION"); #[repr(C)]