From 1876dbdded178195aaaa8f142383d9c04158d0c2 Mon Sep 17 00:00:00 2001 From: Ivan Miletic Date: Thu, 20 Aug 2026 19:33:50 +0200 Subject: [PATCH 1/3] fix: pull-only Azure Pipelines steps template, payload-aware outputs The Azure template no longer bundles a Blob Storage upload: it is now a steps template that only pulls and sets result variables (rulesChanged, rulesProject, rulesTarget, rulesVersion, rulesRelease, rulesSha256), so any destination - S3, Blob, GCS - is one script step the consumer owns, in the same job. Consumers embed it under their own job's steps: and the azureSubscription/storageAccount/container/prefix parameters are gone. The GitHub action gains payload-aware project/target outputs for the same reason: pipelines route on what was pulled without re-parsing the payload or hardcoding names. The GitLab template header's include URL gets the missing x-release-please annotation. Co-Authored-By: Claude Fable 5 --- actions/pull/action.yml | 11 ++ templates/azure-pipelines-pull.yml | 160 +++++++++++++---------------- templates/gitlab-ci-pull.yml | 8 +- 3 files changed, 87 insertions(+), 92 deletions(-) diff --git a/actions/pull/action.yml b/actions/pull/action.yml index f8355ec..2ac0d4e 100644 --- a/actions/pull/action.yml +++ b/actions/pull/action.yml @@ -49,6 +49,12 @@ inputs: default: 0.3.2 # x-release-please-version outputs: + project: + description: Project that was pulled, payload-aware + value: ${{ steps.pull.outputs.project }} + target: + description: Target that was pulled, payload-aware + value: ${{ steps.pull.outputs.target }} changed: description: 'false when the target still matches the current input, true when an artifact was downloaded' value: ${{ steps.pull.outputs.changed }} @@ -116,6 +122,11 @@ runs: [ "$INPUT_UNPACK" = "true" ] && args+=(--unpack) [ "$INPUT_DELETE" = "true" ] && args+=(--delete) + # The effective values, payload-aware, so a workflow routes on what + # was actually pulled instead of re-parsing the payload. + echo "project=$GORULES_PROJECT" >> "$GITHUB_OUTPUT" + echo "target=${GORULES_TARGET:-main}" >> "$GITHUB_OUTPUT" + result="$RUNNER_TEMP/gorules-pull.json" # Exit 3 means the target has not moved. That is a normal outcome for a diff --git a/templates/azure-pipelines-pull.yml b/templates/azure-pipelines-pull.yml index 6f20f5f..01495fb 100644 --- a/templates/azure-pipelines-pull.yml +++ b/templates/azure-pipelines-pull.yml @@ -1,22 +1,26 @@ -# Azure Pipelines template: pull a rules artifact and publish it to Blob Storage. +# Azure Pipelines steps template: pull a rules artifact for your own publish step. # # resources: # repositories: # - repository: gorules # type: github # name: gorules/cli -# ref: refs/tags/cli-v0.2.1 +# ref: refs/tags/cli-v0.3.2 # x-release-please-version # endpoint: # # jobs: -# - template: templates/azure-pipelines-pull.yml@gorules -# parameters: -# url: https://acme.us1.gorules.io -# project: pricing -# target: env:production -# container: rules -# storageAccount: acmerules -# azureSubscription: +# - job: deploy_rules +# pool: +# vmImage: ubuntu-latest +# steps: +# - template: templates/azure-pipelines-pull.yml@gorules +# parameters: +# url: https://acme.us1.gorules.io +# - script: +# +# The template only pulls: it downloads the artifact and sets result variables +# (rulesChanged, rulesProject, rulesTarget, rulesVersion, rulesRelease, +# rulesSha256) for the steps you add after it. Publishing is yours. # # GORULES_TOKEN must exist as a secret variable, either in the pipeline or in a # linked variable group. @@ -30,11 +34,14 @@ parameters: - name: url type: string + # Project and target normally arrive in GRL_PAYLOAD; set them only for + # runs that have no payload (schedules), or pass them at queue time. - name: project type: string + default: '' - name: target type: string - default: main + default: '' - name: out type: string default: $(Build.ArtifactStagingDirectory)/rules @@ -48,91 +55,68 @@ parameters: - name: delete type: boolean default: false - - name: cliVersion - type: string - default: 0.3.2 # x-release-please-version - - name: azureSubscription + # Release or commit id already held; when unchanged, rulesChanged is 'false' + - name: current type: string default: '' - - name: storageAccount - type: string - default: '' - - name: container - type: string - default: '' - - name: prefix + - name: cliVersion type: string - default: rules/live + default: 0.3.2 # x-release-please-version -jobs: - - job: gorules_pull - displayName: Pull rules from GoRules - pool: - vmImage: ubuntu-latest - steps: - # Microsoft-hosted images already ship Node; self-hosted agents may not. - - task: NodeTool@0 - displayName: Use Node 20 - inputs: - versionSpec: '20.x' +steps: + # Microsoft-hosted images already ship Node; self-hosted agents may not. + - task: NodeTool@0 + displayName: Use Node 20 + inputs: + versionSpec: '20.x' - - script: | - set -euo pipefail + - script: | + set -euo pipefail - # BRMS passes GRL_PAYLOAD when it triggers the run, so the same - # template serves a manual run and a webhook-driven one. On a manual - # run the macro below does not resolve and Azure leaves the literal - # '$(GRL_PAYLOAD)' in place, so that exact string means "not set". - if [ -n "${GRL_PAYLOAD:-}" ] && [ "${GRL_PAYLOAD}" != '$(GRL_PAYLOAD)' ]; then - GORULES_PROJECT=$(node -e 'const p=JSON.parse(process.env.GRL_PAYLOAD);process.stdout.write(p.project?.key||p.projectId||"")') - GORULES_TARGET=$(node -e 'const p=JSON.parse(process.env.GRL_PAYLOAD);process.stdout.write(p.target||"main")') - export GORULES_PROJECT GORULES_TARGET - echo "Triggered by BRMS: $GORULES_PROJECT $GORULES_TARGET" - fi + # BRMS passes GRL_PAYLOAD when it triggers the run. On a manual run the + # macro below does not resolve and Azure leaves the literal + # '$(GRL_PAYLOAD)' in place, so that exact string means "not set". + if [ -n "${GRL_PAYLOAD:-}" ] && [ "${GRL_PAYLOAD}" != '$(GRL_PAYLOAD)' ]; then + GORULES_PROJECT=$(node -e 'const p=JSON.parse(process.env.GRL_PAYLOAD);process.stdout.write(p.project?.key||p.projectId||"")') + GORULES_TARGET=$(node -e 'const p=JSON.parse(process.env.GRL_PAYLOAD);process.stdout.write(p.target||"main")') + export GORULES_PROJECT GORULES_TARGET + echo "Triggered by BRMS: $GORULES_PROJECT $GORULES_TARGET" + fi - args=(pull --out "${{ parameters.out }}" --json) - if [ -n "${{ parameters.name }}" ]; then args+=(--name "${{ parameters.name }}"); fi - if [ "${{ lower(parameters.unpack) }}" = "true" ]; then args+=(--unpack); fi - if [ "${{ lower(parameters.delete) }}" = "true" ]; then args+=(--delete); fi + args=(pull --out "${{ parameters.out }}" --json) + if [ -n "${{ parameters.name }}" ]; then args+=(--name "${{ parameters.name }}"); fi + if [ -n "${{ parameters.current }}" ]; then args+=(--current "${{ parameters.current }}"); fi + if [ "${{ lower(parameters.unpack) }}" = "true" ]; then args+=(--unpack); fi + if [ "${{ lower(parameters.delete) }}" = "true" ]; then args+=(--delete); fi - # Exit 3 means the target has not moved: a normal outcome, not a failure - set +e - npx --yes "@gorules/cli@${{ parameters.cliVersion }}" "${args[@]}" > result.json - code=$? - set -e + # Exit 3 means the target has not moved: a normal outcome, not a failure + set +e + npx --yes "@gorules/cli@${{ parameters.cliVersion }}" "${args[@]}" > result.json + code=$? + set -e - if [ "$code" -eq 3 ]; then - echo "##vso[task.setvariable variable=rulesChanged]false" - echo "Target unchanged, nothing downloaded." - exit 0 - fi - [ "$code" -ne 0 ] && exit "$code" + # The effective values, payload-aware, for the steps that follow + echo "##vso[task.setvariable variable=rulesProject]${GORULES_PROJECT:-}" + echo "##vso[task.setvariable variable=rulesTarget]${GORULES_TARGET:-main}" - echo "##vso[task.setvariable variable=rulesChanged]true" - echo "##vso[task.setvariable variable=rulesVersion]$(node -p "require('./result.json').version || ''")" - displayName: gorules pull - env: - GORULES_URL: ${{ parameters.url }} - GORULES_PROJECT: ${{ parameters.project }} - GORULES_TARGET: ${{ parameters.target }} - # Secret variables are NOT mapped into the environment automatically; - # without this line the token is simply absent. - GORULES_TOKEN: $(GORULES_TOKEN) - # Empty on a manual run; set by BRMS when it queues the pipeline - GRL_PAYLOAD: $(GRL_PAYLOAD) + if [ "$code" -eq 3 ]; then + echo "##vso[task.setvariable variable=rulesChanged]false" + echo "Target unchanged, nothing downloaded." + exit 0 + fi + [ "$code" -ne 0 ] && exit "$code" - - task: AzureCLI@2 - displayName: Upload to Blob Storage - condition: and(succeeded(), eq(variables['rulesChanged'], 'true'), ne('${{ parameters.container }}', '')) - inputs: - azureSubscription: ${{ parameters.azureSubscription }} - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - set -euo pipefail - az storage blob upload-batch \ - --account-name "${{ parameters.storageAccount }}" \ - --destination "${{ parameters.container }}/${{ parameters.prefix }}" \ - --source "${{ parameters.out }}" \ - --auth-mode login \ - --overwrite + echo "##vso[task.setvariable variable=rulesChanged]true" + echo "##vso[task.setvariable variable=rulesVersion]$(node -p "require('./result.json').version || ''")" + echo "##vso[task.setvariable variable=rulesRelease]$(node -p "require('./result.json').release || ''")" + echo "##vso[task.setvariable variable=rulesSha256]$(node -p "require('./result.json').sha256 || ''")" + displayName: gorules pull + env: + GORULES_URL: ${{ parameters.url }} + GORULES_PROJECT: ${{ parameters.project }} + GORULES_TARGET: ${{ parameters.target }} + # Secret variables are NOT mapped into the environment automatically; + # without this line the token is simply absent. + GORULES_TOKEN: $(GORULES_TOKEN) + # Empty on a manual run; set by BRMS when it queues the pipeline + GRL_PAYLOAD: $(GRL_PAYLOAD) diff --git a/templates/gitlab-ci-pull.yml b/templates/gitlab-ci-pull.yml index 8aaf8a8..e320170 100644 --- a/templates/gitlab-ci-pull.yml +++ b/templates/gitlab-ci-pull.yml @@ -1,13 +1,13 @@ # GitLab CI template: pull a rules artifact and publish it to your own storage. # # include: -# - remote: 'https://raw.githubusercontent.com/gorules/cli/cli-v0.2.1/templates/gitlab-ci-pull.yml' +# - remote: 'https://raw.githubusercontent.com/gorules/cli/cli-v0.3.2/templates/gitlab-ci-pull.yml' # x-release-please-version # # pull:rules: # extends: .gorules-pull -# variables: -# GORULES_PROJECT: pricing -# GORULES_TARGET: env:production +# # project and target normally arrive in the BRMS webhook payload +# # (GRL_PAYLOAD); set GORULES_PROJECT / GORULES_TARGET only for runs +# # that have no payload, e.g. via the Run pipeline form or a schedule # # GORULES_URL and GORULES_TOKEN come from CI/CD variables; mask and protect the # token. GitLab puts CI/CD variables in the environment automatically, so the From cb2fe1adf0f76cc005cbd2265a5c7a41a3a9115e Mon Sep 17 00:00:00 2001 From: Ivan Miletic Date: Thu, 20 Aug 2026 19:34:25 +0200 Subject: [PATCH 2/3] fix: refresh README pipeline examples and register it with release-please The README's pins were stuck at cli-v0.2.1 - the file was never in extra-files and the version references carried no annotations. The GitLab example also gated its publish job on a dotenv variable in rules:, which GitLab evaluates before any job runs, so the job never ran. Examples now match the payload-driven templates: no baked-in project or target, uploads routed on the payload-aware result variables, Azure shown as the steps template it now is. Co-Authored-By: Claude Fable 5 --- README.md | 53 +++++++++++++++++++++----------------- release-please-config.json | 7 ++++- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 044c51e..8b13f7a 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ esac Pin the version in a pipeline rather than tracking `latest`: ```bash -npx @gorules/cli@0.2.1 pull --project pricing --target env:production +npx @gorules/cli@0.3.2 pull --project pricing --target env:production # x-release-please-version ``` ## GitHub Actions @@ -146,17 +146,19 @@ jobs: rules: runs-on: ubuntu-latest steps: - - uses: gorules/cli/actions/pull@cli-v0.2.1 + - uses: gorules/cli/actions/pull@cli-v0.3.2 # x-release-please-version id: rules with: url: https://acme.us1.gorules.io token: ${{ secrets.GORULES_TOKEN }} - project: pricing - target: env:production + # project and target normally arrive in the BRMS payload; set them + # only for runs that have none (manual without payload, schedules) out: ./dist - - run: aws s3 cp ./dist/ s3://my-bucket/rules/live/ --recursive - if: steps.rules.outputs.changed == 'true' + - name: Deploy + env: + PROJECT: ${{ steps.rules.outputs.project }} + run: aws s3 cp "./dist/$PROJECT" "s3://my-bucket/rules/$PROJECT" ``` | Input | Required | Description | @@ -175,6 +177,7 @@ jobs: | Output | Description | | -------------------------------- | --------------------------------------------------------------- | +| `project` / `target` | What was pulled, payload-aware | | `changed` | `false` when `current` still matched, so nothing was downloaded | | `release` / `version` / `commit` | What the target resolved to | | `sha256` | Checksum of the downloaded artifact | @@ -189,20 +192,19 @@ log. `changed` exists so a scheduled workflow can skip the upload when productio ```yaml include: - - remote: 'https://raw.githubusercontent.com/gorules/cli/cli-v0.2.1/templates/gitlab-ci-pull.yml' + - remote: 'https://raw.githubusercontent.com/gorules/cli/cli-v0.3.2/templates/gitlab-ci-pull.yml' # x-release-please-version pull:rules: extends: .gorules-pull - variables: - GORULES_PROJECT: pricing - GORULES_TARGET: env:production + # project and target normally arrive in the BRMS payload (GRL_PAYLOAD); + # set GORULES_PROJECT / GORULES_TARGET only for runs that have none publish:rules: needs: ['pull:rules'] - rules: - - if: $RULES_CHANGED == "true" script: - - aws s3 cp dist/ s3://my-bucket/rules/live/ --recursive + # dotenv variables are not visible in rules: (evaluated before jobs run) - + # gate in script when using scheduled pulls with GORULES_CURRENT + - aws s3 cp "dist/$RULES_PROJECT" "s3://my-bucket/rules/$RULES_PROJECT" ``` `GORULES_URL` and `GORULES_TOKEN` are CI/CD variables; mask and protect the token. GitLab puts them @@ -217,7 +219,9 @@ anything. ## Azure Pipelines -`templates/azure-pipelines-pull.yml` is a job template you can reference directly: +`templates/azure-pipelines-pull.yml` is a steps template: it pulls the artifact and sets result +variables (`rulesChanged`, `rulesProject`, `rulesTarget`, `rulesVersion`, `rulesRelease`, +`rulesSha256`), and you append your own publish step in the same job: ```yaml resources: @@ -225,18 +229,21 @@ resources: - repository: gorules type: github name: gorules/cli - ref: refs/tags/cli-v0.2.1 + ref: refs/tags/cli-v0.3.2 # x-release-please-version endpoint: jobs: - - template: templates/azure-pipelines-pull.yml@gorules - parameters: - url: https://acme.us1.gorules.io - project: pricing - target: env:production - azureSubscription: - storageAccount: acmerules - container: rules + - job: deploy_rules + pool: + vmImage: ubuntu-latest + steps: + - template: templates/azure-pipelines-pull.yml@gorules + parameters: + url: https://acme.us1.gorules.io + # project and target normally arrive in the BRMS payload (GRL_PAYLOAD) + + - script: aws s3 cp "$(Build.ArtifactStagingDirectory)/rules/$(rulesProject)" "s3://my-bucket/rules/$(rulesProject)" + displayName: Deploy ``` `GORULES_TOKEN` must exist as a secret pipeline variable or in a linked variable group. Azure diff --git a/release-please-config.json b/release-please-config.json index 2f44904..bf74f02 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,7 +4,12 @@ ".": { "package-name": "@gorules/cli", "release-type": "node", - "extra-files": ["actions/pull/action.yml", "templates/gitlab-ci-pull.yml", "templates/azure-pipelines-pull.yml"] + "extra-files": [ + "actions/pull/action.yml", + "templates/gitlab-ci-pull.yml", + "templates/azure-pipelines-pull.yml", + "README.md" + ] } } } From fc6050191142fef3e88ddd9b1b19b592adcfb062 Mon Sep 17 00:00:00 2001 From: Ivan Miletic Date: Thu, 20 Aug 2026 19:37:20 +0200 Subject: [PATCH 3/3] fix: run pipeline templates on Node 24 Node 20 reached end-of-life in April 2026. Node 24 is the active LTS, matches this repository's own CI, and is available identically on all three platforms: Docker Hub for the GitLab image, nodejs.org via NodeTool for Azure, and preinstalled on GitHub runners (the action pins nothing and uses the runner's Node). Co-Authored-By: Claude Fable 5 --- templates/azure-pipelines-pull.yml | 4 ++-- templates/gitlab-ci-pull.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/azure-pipelines-pull.yml b/templates/azure-pipelines-pull.yml index 01495fb..be7071d 100644 --- a/templates/azure-pipelines-pull.yml +++ b/templates/azure-pipelines-pull.yml @@ -66,9 +66,9 @@ parameters: steps: # Microsoft-hosted images already ship Node; self-hosted agents may not. - task: NodeTool@0 - displayName: Use Node 20 + displayName: Use Node 24 inputs: - versionSpec: '20.x' + versionSpec: '24.x' - script: | set -euo pipefail diff --git a/templates/gitlab-ci-pull.yml b/templates/gitlab-ci-pull.yml index e320170..f6def16 100644 --- a/templates/gitlab-ci-pull.yml +++ b/templates/gitlab-ci-pull.yml @@ -18,7 +18,7 @@ # and a webhook-driven one. .gorules-pull: - image: node:20-alpine + image: node:24-alpine variables: GORULES_OUT: dist GORULES_CLI_VERSION: '0.3.2' # x-release-please-version