Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions .github/workflows/upstream.yml
Original file line number Diff line number Diff line change
@@ -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 '<details><summary>Verification output</summary>\n\n```\n'
sed -E 's/\x1b\[[0-9;]*[A-Za-z]//g' "${RUNNER_TEMP}/verify.log" | tail -c 8000
printf '\n```\n\n</details>\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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

[package]
name = "lightningcss-ffi"
version = "0.2.0"
edition = "2021"
authors = ["Marco Roth <marco.roth@intergga.ch>"]
description = "C FFI bindings for Lightning CSS, used by the lightningcss gem"
Expand All @@ -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"

Expand Down
22 changes: 22 additions & 0 deletions rust/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down