diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..75ccb9f --- /dev/null +++ b/.actrc @@ -0,0 +1,3 @@ +-P ubuntu-latest=catthehacker/ubuntu:act-latest +-P ubuntu-24.04=catthehacker/ubuntu:act-latest +-P ubuntu-22.04=catthehacker/ubuntu:act-latest diff --git a/.docker/act/Dockerfile b/.docker/act/Dockerfile new file mode 100644 index 0000000..081c37e --- /dev/null +++ b/.docker/act/Dockerfile @@ -0,0 +1,20 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG TARGETARCH + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl tar git \ + && rm -rf /var/lib/apt/lists/* + +RUN set -eu; \ + case "${TARGETARCH}" in \ + amd64) ACT_ARCH="x86_64" ;; \ + arm64) ACT_ARCH="arm64" ;; \ + *) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + curl -fsSL "https://github.com/nektos/act/releases/latest/download/act_Linux_${ACT_ARCH}.tar.gz" \ + | tar -xz -C /usr/local/bin act; \ + chmod +x /usr/local/bin/act + +ENTRYPOINT ["act"] diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml new file mode 100644 index 0000000..54d4804 --- /dev/null +++ b/.github/actions/setup-dependencies/action.yml @@ -0,0 +1,35 @@ +name: Setup dependencies +description: Setup Node, configure Yarn, restore Yarn cache, install dependencies, and expose node_modules/.bin + +inputs: + node-version: + description: Node.js version passed to actions/setup-node + required: true + +runs: + using: composite + steps: + - name: Use Node.js ${{ inputs.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Yarn configuration + shell: bash + run: make .yarnrc.yml + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: .yarn + key: ${{ runner.OS }}-node-${{ inputs.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.OS }}-node-${{ inputs.node-version }}-yarn- + + - name: Install dependencies + shell: bash + run: yarn install + + - name: Fix PATH for hoisting + shell: bash + run: echo "${{ github.workspace }}/node_modules/.bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 018469d..23416f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,25 +23,16 @@ jobs: with: fetch-depth: 0 - - name: Use Node.js - uses: actions/setup-node@v4 + - name: Setup dependencies + uses: ./.github/actions/setup-dependencies with: node-version: '24.x' - - name: Yarn configuration - run: make .yarnrc.yml - - name: Configure Git run: | git config --global user.email "integration@retailcrm.ru" git config --global user.name "RetailCRM.CI" - - name: Install dependencies - run: yarn install - - - name: Fix PATH for hoisting - run: echo "${{ github.workspace }}/node_modules/.bin" >> $GITHUB_PATH - - name: Build worktree run: yarn workspaces foreach -A --topological-dev run build @@ -64,7 +55,7 @@ jobs: - name: Run release if: ${{ inputs.prerelease == 'none' }} run: | - echo "YARN_ENABLE_IMMUTABLE_INSTALLS=false" >> $GITHUB_ENV + echo "YARN_ENABLE_IMMUTABLE_INSTALLS=false" >> "$GITHUB_ENV" yes | npx tsx scripts/release.ts - name: Push tags to repository @@ -83,7 +74,7 @@ jobs: id: version run: | VERSION=$(grep -oP '"version":\s*"\K[^"]+' package.json) - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "${VERSION}" outputs: @@ -97,20 +88,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install Node.js - uses: actions/setup-node@v4 + - name: Setup dependencies + uses: ./.github/actions/setup-dependencies with: node-version: '24.x' - - name: Yarn configuration - run: make .yarnrc.yml - - - name: Install dependencies - run: yarn install - - - name: Fix PATH for hoisting - run: echo "${{ github.workspace }}/node_modules/.bin" >> $GITHUB_PATH - - name: Build Storybook run: yarn workspace @retailcrm/embed-ui-v1-components run storybook:build @@ -122,7 +104,7 @@ jobs: - name: Push version forward id: version - run: echo "version=${{ needs.release.outputs.version }}" >> $GITHUB_OUTPUT + run: echo "version=${{ needs.release.outputs.version }}" >> "$GITHUB_OUTPUT" outputs: version: ${{ steps.version.outputs.version }} @@ -142,7 +124,7 @@ jobs: path: packages/v1-components/storybook/dist - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: packages/v1-components/storybook/dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b9322c6..b519b67 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,13 +1,28 @@ name: Tests -on: [ push, pull_request ] +on: + pull_request: + push: + branches: + - master concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: + workflow-lint: + runs-on: ubuntu-latest + + steps: + - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. + uses: actions/checkout@v4 + + - name: Run actionlint + uses: devops-actions/actionlint@v0.1.10 + eslint: + needs: workflow-lint runs-on: ubuntu-latest strategy: @@ -18,29 +33,11 @@ jobs: - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - name: Setup dependencies + uses: ./.github/actions/setup-dependencies with: node-version: ${{ matrix.node-version }} - - name: Yarn configuration - run: make .yarnrc.yml - - - name: Cache dependencies - id: cache-deps - uses: actions/cache@v4 - with: - path: .yarn - key: ${{ runner.OS }}-node-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.OS }}-node-${{ matrix.node-version }}-yarn- - - - name: Install dependencies - run: yarn install - - - name: Fix PATH for hoisting - run: echo "${{ github.workspace }}/node_modules/.bin" >> $GITHUB_PATH - - name: Build worktree run: yarn workspaces foreach -A --topological-dev run build @@ -62,29 +59,11 @@ jobs: - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}. uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - name: Setup dependencies + uses: ./.github/actions/setup-dependencies with: node-version: ${{ matrix.node-version }} - - name: Yarn configuration - run: make .yarnrc.yml - - - name: Cache dependencies - id: cache-deps - uses: actions/cache@v4 - with: - path: .yarn - key: ${{ runner.OS }}-node-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.OS }}-node-${{ matrix.node-version }}-yarn- - - - name: Install dependencies - run: yarn install - - - name: Fix PATH for hoisting - run: echo "${{ github.workspace }}/node_modules/.bin" >> $GITHUB_PATH - - name: Build worktree run: yarn workspaces foreach -A --topological-dev run build diff --git a/Makefile b/Makefile index b7c67db..4427bd0 100644 --- a/Makefile +++ b/Makefile @@ -1,72 +1,210 @@ +.DEFAULT_GOAL := help + TARGET_HEADER=@echo -e '===== \e[34m' $@ '\e[0m' -YARN=docker-compose run --rm node yarn +TARGET_OK=@echo -e '\e[32mOK\e[0m' +COMPOSE=$(shell if command -v docker-compose >/dev/null 2>&1; then echo "docker-compose"; elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then echo "docker compose"; fi) +YARN=$(COMPOSE) run --rm node yarn + +.PHONY: .require-compose +.require-compose: + @if [ -z "$(COMPOSE)" ]; then \ + echo "docker compose is unavailable (need 'docker compose' or 'docker-compose')"; \ + exit 1; \ + fi .PHONY: .yarnrc.yml -.yarnrc.yml: ## Generates yarn configuration +.yarnrc.yml: ## [Setup][local] Generates yarn configuration $(TARGET_HEADER) cp .yarnrc.dist.yml .yarnrc.yml + $(TARGET_OK) .PHONY: node_modules -node_modules: package.json yarn.lock ## Installs dependencies +node_modules: .require-compose package.json yarn.lock ## [Setup][docker][heavy] Installs dependencies $(TARGET_HEADER) - @docker-compose run --rm node yarn install --silent + @$(COMPOSE) run --rm node yarn install --silent + $(TARGET_OK) .PHONY: build -build: ## Builds the package +build: .require-compose ## [Build][docker][heavy] Builds all workspaces $(TARGET_HEADER) $(YARN) workspaces foreach -A --topological-dev run build + $(TARGET_OK) -.PHONY: build -prepare: ## Builds the package +.PHONY: prepare +prepare: .require-compose ## [Build][docker][heavy] Runs prepare in all workspaces $(TARGET_HEADER) $(YARN) workspaces foreach -A --topological-dev run prepare + $(TARGET_OK) .PHONY: release -release: ## Bumps version and creates tag +release: .require-compose ## [Release][docker][heavy][network] Bumps version and creates tag $(TARGET_HEADER) ifdef as $(YARN) release:$(as) else $(YARN) release endif + $(TARGET_OK) .PHONY: tests -tests: ## Runs autotests +tests: .require-compose ## [Tests][docker] Runs autotests $(TARGET_HEADER) ifdef cli $(YARN) test $(cli) --passWithNoTests else $(YARN) test endif + $(TARGET_OK) .PHONY: tests-coverage -tests-coverage: ## Runs autotests with coverage report +tests-coverage: .require-compose ## [Tests][docker][heavy] Runs autotests with coverage report $(TARGET_HEADER) ifdef cli $(YARN) vitest --run --coverage $(cli) --passWithNoTests else $(YARN) test:coverage endif + $(TARGET_OK) .PHONY: tests-typecheck-contexts -tests-typecheck-contexts: ## Runs typecheck tests (test-d.ts) for v1-contexts +tests-typecheck-contexts: .require-compose ## [Tests][docker] Runs typecheck tests (test-d.ts) for v1-contexts $(TARGET_HEADER) ifdef cli $(YARN) vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json $(cli) else $(YARN) vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json endif + $(TARGET_OK) .PHONY: tests-typecheck-v1-contexts -tests-typecheck-v1-contexts: tests-typecheck-contexts ## Alias for tests-typecheck-contexts +tests-typecheck-v1-contexts: tests-typecheck-contexts ## [Tests][alias] Alias for tests-typecheck-contexts .PHONY: tests-typecheck -tests-typecheck: tests-typecheck-contexts ## Runs typecheck tests (currently v1-contexts) +tests-typecheck: tests-typecheck-contexts ## [Tests][alias] Runs typecheck tests (currently v1-contexts) + +.PHONY: ci-actionlint +ci-actionlint: ## [CI][docker] Lints GitHub Actions workflows locally (actionlint binary or docker image) + $(TARGET_HEADER) + @if command -v actionlint >/dev/null 2>&1; then \ + actionlint; \ + elif [ -n "$(COMPOSE)" ]; then \ + $(COMPOSE) run --rm actionlint; \ + elif command -v docker >/dev/null 2>&1; then \ + docker run --rm -v "$$(pwd):/repo" -w /repo rhysd/actionlint:latest; \ + else \ + echo "actionlint is not installed and docker/docker-compose is unavailable"; \ + exit 1; \ + fi + $(TARGET_OK) + +.PHONY: ci-act-plan +ci-act-plan: ## [CI][docker] Shows act execution plan for tests workflow without running jobs + $(TARGET_HEADER) + @if command -v act >/dev/null 2>&1; then \ + act -n pull_request -W .github/workflows/tests.yml; \ + elif [ -n "$(COMPOSE)" ]; then \ + $(COMPOSE) run --rm --build act -n pull_request -W .github/workflows/tests.yml; \ + else \ + echo "act is not installed and docker compose is unavailable"; \ + exit 1; \ + fi + $(TARGET_OK) + +.PHONY: ci-act-tests +ci-act-tests: ## [CI][docker][heavy][network] Runs tests workflow locally via act + $(TARGET_HEADER) + @if command -v act >/dev/null 2>&1; then \ + act pull_request -W .github/workflows/tests.yml -j workflow-lint -j eslint -j tests; \ + elif [ -n "$(COMPOSE)" ]; then \ + $(COMPOSE) run --rm --build act pull_request -W .github/workflows/tests.yml -j workflow-lint -j eslint -j tests; \ + else \ + echo "act is not installed and docker compose is unavailable"; \ + exit 1; \ + fi + $(TARGET_OK) .PHONY: help -help: ## Calls recipes list - @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk '\ - BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +help: ## [General] Shows grouped command help + @set -eu; \ + if [ -t 1 ] && [ -z "$${CI:-}" ] && [ -z "$${NO_COLOR:-}" ]; then \ + C_HEAD="$$(printf '\033[1;36m')"; \ + C_TGT="$$(printf '\033[36m')"; \ + C_TAG="$$(printf '\033[33m')"; \ + C_WARN="$$(printf '\033[33m')"; \ + C_RST="$$(printf '\033[0m')"; \ + else \ + C_HEAD=''; C_TGT=''; C_TAG=''; C_WARN=''; C_RST=''; \ + fi; \ + FILTER="$$(printf '%s' "$(value filter)" | tr '[:upper:]' '[:lower:]')"; \ + SHOW_INTERNAL="$(value show_internal)"; \ + awk -v filter="$$FILTER" -v show_internal="$$SHOW_INTERNAL" '\ + function trim(s){ sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$$/, "", s); return s } \ + function group_order(g){ \ + if (g=="General") return 1; \ + if (g=="Setup") return 2; \ + if (g=="Build") return 3; \ + if (g=="Tests") return 4; \ + if (g=="CI") return 5; \ + if (g=="Release") return 6; \ + return 99; \ + } \ + /^[a-zA-Z0-9_.-]+:.*##[[:space:]]+/ { \ + target = $$1; sub(/:.*/, "", target); \ + if (show_internal != "1" && target ~ /^\./) next; \ + desc = $$0; sub(/^.*##[[:space:]]*/, "", desc); \ + group = "Other"; tags = ""; \ + if (match(desc, /^\[[^]]+\]/)) { \ + group = substr(desc, 2, RLENGTH - 2); \ + desc = substr(desc, RLENGTH + 1); \ + } \ + while (match(desc, /^[[:space:]]*\[[^]]+\]/)) { \ + sub(/^[[:space:]]*/, "", desc); \ + rb = index(desc, "]"); \ + if (rb == 0) break; \ + tags = tags "[" substr(desc, 2, rb - 2) "]"; \ + desc = substr(desc, rb + 1); \ + } \ + desc = trim(desc); \ + hay = tolower(target " " group " " tags " " desc); \ + if (filter != "" && index(hay, filter) == 0) next; \ + printf "%02d\t%s\t%s\t%s\t%s\n", group_order(group), group, target, tags, desc; \ + } \ + ' $(MAKEFILE_LIST) \ + | sort -t "$$(printf '\t')" -k1,1n -k3,3 \ + | awk -F '\t' -v c_head="$$C_HEAD" -v c_tgt="$$C_TGT" -v c_tag="$$C_TAG" -v c_rst="$$C_RST" '\ + BEGIN { current = ""; count = 0 } \ + { \ + if ($$2 != current) { \ + if (current != "") print ""; \ + printf "%s%s%s\n", c_head, $$2, c_rst; \ + current = $$2; \ + } \ + printf " %s%-30s%s %s", c_tgt, $$3, c_rst, $$5; \ + if ($$4 != "") printf " %s%s%s", c_tag, $$4, c_rst; \ + printf "\n"; \ + count++; \ + } \ + END { if (count == 0) print "No targets matched the current filter." }'; \ + echo ""; \ + printf "%sQuick Start%s\n" "$$C_HEAD" "$$C_RST"; \ + printf " make ci-actionlint\n"; \ + printf " make tests\n"; \ + printf " make tests-coverage\n"; \ + printf " make ci-act-plan\n"; \ + echo ""; \ + printf "%sExamples%s\n" "$$C_HEAD" "$$C_RST"; \ + printf " make help filter=ci\n"; \ + printf " make help show_internal=1\n"; \ + printf " make tests cli='tests/scenarios/customer/phone.test.ts'\n"; \ + printf " make release as=beta\n"; \ + dup_targets="$$(awk -F: '/^[a-zA-Z0-9_.-]+:/ && $$1 != ".PHONY" {print $$1}' $(MAKEFILE_LIST) | sort | uniq -d | tr '\n' ' ')"; \ + dup_phony="$$(awk '/^\.PHONY:[[:space:]]*/ {for (i=2; i<=NF; i++) print $$i}' $(MAKEFILE_LIST) | sort | uniq -d | tr '\n' ' ')"; \ + if [ -n "$$dup_targets$$dup_phony" ]; then \ + echo ""; \ + printf "%sWarnings%s\n" "$$C_WARN" "$$C_RST"; \ + if [ -n "$$dup_targets" ]; then printf " duplicate targets: %s\n" "$$dup_targets"; fi; \ + if [ -n "$$dup_phony" ]; then printf " duplicate .PHONY entries: %s\n" "$$dup_phony"; fi; \ + fi # Colors $(call computable,CC_BLACK,$(shell tput -Txterm setaf 0 2>/dev/null)) diff --git a/docker-compose.yml b/docker-compose.yml index 015f994..38a235c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,3 +20,22 @@ services: traefik.http.routers.embed-ui-v1-components.service: embed-ui-v1-components traefik.http.services.embed-ui-v1-components.loadbalancer.server.port: '6006' dev.orbstack.domains: 'v1.embed-ui.local' + + actionlint: + image: rhysd/actionlint:latest + volumes: + - ./:/project + working_dir: /project + entrypoint: [ "actionlint" ] + + act: + build: + context: ./ + dockerfile: .docker/act/Dockerfile + environment: + - DOCKER_HOST=unix:///var/run/docker.sock + volumes: + - ./:/project + - /var/run/docker.sock:/var/run/docker.sock + working_dir: /project + entrypoint: [ "act" ]