diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b303066..478e500 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @Justus-at-Tazama @Sandy-at-Tazama @scott45 +* @tazama-lf/core-codeowners @tazama-lf/community-codeowners diff --git a/.github/workflows/package-rule-rc.yml b/.github/workflows/package-rule-rc.yml new file mode 100644 index 0000000..18b82cf --- /dev/null +++ b/.github/workflows/package-rule-rc.yml @@ -0,0 +1,197 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Reusable workflow: builds and pushes an RC Docker image for a rule processor. +# +# Each rule repo calls this workflow with its own rule_number and rule_org rather +# than maintaining a full copy of the job definition locally. +# +# Caller stub example (place in each rule repo's .github/workflows/package-rule-rc.yml): +# +# on: +# push: +# branches: [dev] +# workflow_dispatch: +# jobs: +# build: +# uses: tazama-lf/workflows/.github/workflows/package-rule-rc.yml@dev +# with: +# rule_number: "901" +# rule_org: "tazama-lf" +# secrets: inherit +# +# Please do not attempt to edit this flow without the direct consent from the DevOps team. +# This file is managed centrally. + +name: Rule Executer - Rule processor RC automation (reusable) + +permissions: + contents: read + +on: + workflow_call: + inputs: + rule_number: + description: 'Zero-padded rule number (e.g. "001", "901")' + required: true + type: string + rule_org: + description: 'GitHub org that owns the rule repo ("tazama-lf" or "frmscoe")' + required: true + type: string + secrets: + GH_TOKEN_LIB: + required: true + DOCKER_USERNAME: + required: true + DOCKER_PASSWORD: + required: true + SLACK_WEBHOOK_URL: + required: true + +jobs: + automate-rule-executer: + if: github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' + runs-on: ubuntu-latest + + steps: + - name: Checkout rule repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Read rule version from package.json + id: rule_version + run: | + VERSION=$(jq -r '.version' package.json) + if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then + echo "❌ Could not read version from package.json" + exit 1 + fi + if [[ "$VERSION" != *-* ]]; then + echo "❌ RC package-rule-rc.yml triggered but version '$VERSION' is not a prerelease." + echo " Expected a version with a prerelease suffix (e.g. 1.0.0-rc.1)." + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "Rule version: $VERSION" + + - name: Clone Rule Executer repository (dev branch) + run: | + git clone https://github.com/tazama-lf/rule-executer -b dev rule-executer + echo "Rule Executer clone complete." + + - name: Prepare rule-executer-${{ inputs.rule_number }} + run: | + cp -R rule-executer "rule-executer-${{ inputs.rule_number }}" + echo "Created rule-executer-${{ inputs.rule_number }}" + + - name: Modify package.json and Dockerfile for rule ${{ inputs.rule_number }} + run: | + RULE_DIR="rule-executer-${{ inputs.rule_number }}" + RULE_NUM="${{ inputs.rule_number }}" + RULE_ORG="${{ inputs.rule_org }}" + VERSION="${{ steps.rule_version.outputs.VERSION }}" + + echo "Applying substitutions for rule-${RULE_NUM} (org: ${RULE_ORG}, version: ${VERSION})" + + # Update rule dependency in package.json: + # "rule": "npm:@tazama-lf/rule-901@X.Y.Z" + # → for tazama-lf: keep @tazama-lf, change rule number and version + # → for frmscoe: switch namespace to @frmscoe, change rule number and version + if [ "$RULE_ORG" = "frmscoe" ]; then + sed -i "s|npm:@tazama-lf/rule-[^@]*@[^\"]*|npm:@frmscoe/rule-${RULE_NUM}@${VERSION}|g" "${RULE_DIR}/package.json" + else + sed -i "s|npm:@tazama-lf/rule-[^@]*@[^\"]*|npm:@tazama-lf/rule-${RULE_NUM}@${VERSION}|g" "${RULE_DIR}/package.json" + fi + + # Validate rule dependency rewrite succeeded — fail if pattern didn't match + EXPECTED_SCOPE=$( [ "$RULE_ORG" = "frmscoe" ] && echo "@frmscoe" || echo "@tazama-lf" ) + if ! grep -q "npm:${EXPECTED_SCOPE}/rule-${RULE_NUM}@${VERSION}" "${RULE_DIR}/package.json"; then + echo "❌ Failed to update rule dependency in package.json — pattern may have changed" + echo " Expected: npm:${EXPECTED_SCOPE}/rule-${RULE_NUM}@${VERSION}" + grep '"rule"' "${RULE_DIR}/package.json" || echo " (no 'rule' key found)" + exit 1 + fi + + # Update RULE_NAME and APM_SERVICE_NAME in Dockerfile (flexible pattern — not hardcoded to 901) + sed -i "s/ENV RULE_NAME=\"[^\"]*\"/ENV RULE_NAME=\"${RULE_NUM}\"/" "${RULE_DIR}/Dockerfile" + sed -i "s/ENV APM_SERVICE_NAME=rule-[^ ]*/ENV APM_SERVICE_NAME=rule-${RULE_NUM}/" "${RULE_DIR}/Dockerfile" + + echo "=== package.json rule dep after substitution ===" + grep '"rule"' "${RULE_DIR}/package.json" + echo "=== Dockerfile RULE_NAME after substitution ===" + grep 'RULE_NAME\|APM_SERVICE_NAME' "${RULE_DIR}/Dockerfile" + + # Validate substitutions actually occurred — fail fast if template changed upstream + if ! grep -q "ENV RULE_NAME=\"${RULE_NUM}\"" "${RULE_DIR}/Dockerfile"; then + echo "❌ Failed to update RULE_NAME in Dockerfile — template may have changed" + exit 1 + fi + if ! grep -q "ENV APM_SERVICE_NAME=rule-${RULE_NUM}" "${RULE_DIR}/Dockerfile"; then + echo "❌ Failed to update APM_SERVICE_NAME in Dockerfile — template may have changed" + exit 1 + fi + + - name: Install dependencies + run: | + cd "rule-executer-${{ inputs.rule_number }}" + npm ci + env: + GH_TOKEN: ${{ secrets.GH_TOKEN_LIB }} + + - name: Build and push RC Docker image + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: | + VERSION="${{ steps.rule_version.outputs.VERSION }}" + RULE_NUM="${{ inputs.rule_number }}" + IMAGE="tazamaorg/rule-${RULE_NUM}" + + echo "$DOCKER_PASSWORD" | docker login --username "$DOCKER_USERNAME" --password-stdin + + # Build once, tag with versioned prerelease and moving :rc pointer + docker build -t "${IMAGE}:${VERSION}" -t "${IMAGE}:rc" "rule-executer-${RULE_NUM}" + + docker push "${IMAGE}:${VERSION}" + docker push "${IMAGE}:rc" + + docker rmi "${IMAGE}:${VERSION}" "${IMAGE}:rc" + echo "RC image pushed: ${IMAGE}:${VERSION} and ${IMAGE}:rc" + + - name: Send Slack notification + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + curl -s -X POST -H 'Content-type: application/json' --data '{ + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "New Rule RC Docker image published :ship:", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Rule:*\nrule-${{ inputs.rule_number }}" + }, + { + "type": "mrkdwn", + "text": "*Version:*\n${{ steps.rule_version.outputs.VERSION }}" + }, + { + "type": "mrkdwn", + "text": "*DockerHub:*\n" + } + ] + } + ] + }' "$SLACK_WEBHOOK_URL" || true diff --git a/.github/workflows/package-rule.yml b/.github/workflows/package-rule.yml new file mode 100644 index 0000000..fdc0a5c --- /dev/null +++ b/.github/workflows/package-rule.yml @@ -0,0 +1,197 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Reusable workflow: builds and pushes a stable Docker image for a rule processor. +# +# Triggered on merge to main in the rule repo (stable release). Produces both a +# versioned tag and the :latest moving pointer. +# +# Each rule repo calls this workflow with its own rule_number and rule_org rather +# than maintaining a full copy of the job definition locally. +# +# Caller stub example (place in each rule repo's .github/workflows/package-rule.yml): +# +# on: +# push: +# branches: [main] +# workflow_dispatch: +# jobs: +# build: +# uses: tazama-lf/workflows/.github/workflows/package-rule.yml@dev +# with: +# rule_number: "901" +# rule_org: "tazama-lf" +# secrets: inherit +# +# Please do not attempt to edit this flow without the direct consent from the DevOps team. +# This file is managed centrally. + +name: Rule Executer - Rule processor stable automation (reusable) + +permissions: + contents: read + +on: + workflow_call: + inputs: + rule_number: + description: 'Zero-padded rule number (e.g. "001", "901")' + required: true + type: string + rule_org: + description: 'GitHub org that owns the rule repo ("tazama-lf" or "frmscoe")' + required: true + type: string + secrets: + GH_TOKEN_LIB: + required: true + DOCKER_USERNAME: + required: true + DOCKER_PASSWORD: + required: true + SLACK_WEBHOOK_URL: + required: true + +jobs: + automate-rule-executer: + if: github.actor != 'dependabot[bot]' && github.actor != 'dependabot-preview[bot]' + runs-on: ubuntu-latest + + steps: + - name: Checkout rule repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Read rule version from package.json + id: rule_version + run: | + VERSION=$(jq -r '.version' package.json) + if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then + echo "❌ Could not read version from package.json" + exit 1 + fi + if [[ "$VERSION" == *-* ]]; then + echo "❌ Stable package-rule.yml triggered but version '$VERSION' is a prerelease." + echo " Merge to main should be blocked by version-check.yml — investigate branch protection." + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "Rule version: $VERSION" + + - name: Clone Rule Executer repository (main branch) + run: | + git clone https://github.com/tazama-lf/rule-executer -b main rule-executer + echo "Rule Executer clone complete." + + - name: Prepare rule-executer-${{ inputs.rule_number }} + run: | + cp -R rule-executer "rule-executer-${{ inputs.rule_number }}" + echo "Created rule-executer-${{ inputs.rule_number }}" + + - name: Modify package.json and Dockerfile for rule ${{ inputs.rule_number }} + run: | + RULE_DIR="rule-executer-${{ inputs.rule_number }}" + RULE_NUM="${{ inputs.rule_number }}" + RULE_ORG="${{ inputs.rule_org }}" + VERSION="${{ steps.rule_version.outputs.VERSION }}" + + echo "Applying substitutions for rule-${RULE_NUM} (org: ${RULE_ORG}, version: ${VERSION})" + + # Update rule dependency in package.json + if [ "$RULE_ORG" = "frmscoe" ]; then + sed -i "s|npm:@tazama-lf/rule-[^@]*@[^\"]*|npm:@frmscoe/rule-${RULE_NUM}@${VERSION}|g" "${RULE_DIR}/package.json" + else + sed -i "s|npm:@tazama-lf/rule-[^@]*@[^\"]*|npm:@tazama-lf/rule-${RULE_NUM}@${VERSION}|g" "${RULE_DIR}/package.json" + fi + + # Validate rule dependency rewrite succeeded — fail if pattern didn't match + EXPECTED_SCOPE=$( [ "$RULE_ORG" = "frmscoe" ] && echo "@frmscoe" || echo "@tazama-lf" ) + if ! grep -q "npm:${EXPECTED_SCOPE}/rule-${RULE_NUM}@${VERSION}" "${RULE_DIR}/package.json"; then + echo "❌ Failed to update rule dependency in package.json — pattern may have changed" + echo " Expected: npm:${EXPECTED_SCOPE}/rule-${RULE_NUM}@${VERSION}" + grep '"rule"' "${RULE_DIR}/package.json" || echo " (no 'rule' key found)" + exit 1 + fi + + # Update RULE_NAME and APM_SERVICE_NAME in Dockerfile (flexible pattern — not hardcoded to 901) + sed -i "s/ENV RULE_NAME=\"[^\"]*\"/ENV RULE_NAME=\"${RULE_NUM}\"/" "${RULE_DIR}/Dockerfile" + sed -i "s/ENV APM_SERVICE_NAME=rule-[^ ]*/ENV APM_SERVICE_NAME=rule-${RULE_NUM}/" "${RULE_DIR}/Dockerfile" + + echo "=== package.json rule dep after substitution ===" + grep '"rule"' "${RULE_DIR}/package.json" + echo "=== Dockerfile RULE_NAME after substitution ===" + grep 'RULE_NAME\|APM_SERVICE_NAME' "${RULE_DIR}/Dockerfile" + + # Validate substitutions actually occurred — fail fast if template changed upstream + if ! grep -q "ENV RULE_NAME=\"${RULE_NUM}\"" "${RULE_DIR}/Dockerfile"; then + echo "❌ Failed to update RULE_NAME in Dockerfile — template may have changed" + exit 1 + fi + if ! grep -q "ENV APM_SERVICE_NAME=rule-${RULE_NUM}" "${RULE_DIR}/Dockerfile"; then + echo "❌ Failed to update APM_SERVICE_NAME in Dockerfile — template may have changed" + exit 1 + fi + + - name: Install dependencies + run: | + cd "rule-executer-${{ inputs.rule_number }}" + npm ci + env: + GH_TOKEN: ${{ secrets.GH_TOKEN_LIB }} + + - name: Build and push stable Docker image + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: | + VERSION="${{ steps.rule_version.outputs.VERSION }}" + RULE_NUM="${{ inputs.rule_number }}" + IMAGE="tazamaorg/rule-${RULE_NUM}" + + echo "$DOCKER_PASSWORD" | docker login --username "$DOCKER_USERNAME" --password-stdin + + # Build once, tag with versioned and :latest moving pointer + docker build -t "${IMAGE}:${VERSION}" -t "${IMAGE}:latest" "rule-executer-${RULE_NUM}" + + docker push "${IMAGE}:${VERSION}" + docker push "${IMAGE}:latest" + + docker rmi "${IMAGE}:${VERSION}" "${IMAGE}:latest" + echo "Stable image pushed: ${IMAGE}:${VERSION} and ${IMAGE}:latest" + + - name: Send Slack notification + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + curl -s -X POST -H 'Content-type: application/json' --data '{ + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "New Rule stable Docker image published :white_check_mark:", + "emoji": true + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Rule:*\nrule-${{ inputs.rule_number }}" + }, + { + "type": "mrkdwn", + "text": "*Version:*\n${{ steps.rule_version.outputs.VERSION }}" + }, + { + "type": "mrkdwn", + "text": "*DockerHub:*\n" + } + ] + } + ] + }' "$SLACK_WEBHOOK_URL" || true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b62fa6c..f4254c5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,139 +1,82 @@ # SPDX-License-Identifier: Apache-2.0 -name: Publish dev npm package to GitHub +# This workflow publishes an npm package to the GitHub Package Registry (@frmscoe scope). +# +# Versioning policy: +# The version in package.json is the only signal used to determine the dist-tag: +# - Prerelease (X.Y.Z-rc.N) → published under the 'rc' dist-tag (triggered manually from dev) +# - Stable (X.Y.Z) → published under 'latest' dist-tag (triggered automatically on merge to main) +# +# Developers set the version in package.json manually. The version-check.yml workflow +# enforces that no prerelease version can merge to main. +# +# Please do not attempt to edit this flow without the direct consent from the DevOps team. +# This file is managed centrally. + +name: Publish npm package to GitHub Packages on: push: branches: - - 'dev' - paths-ignore: - - package.json - - package-lock.json + - main workflow_dispatch: jobs: build-and-publish: runs-on: ubuntu-latest - env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN_LIB }} permissions: packages: write contents: read steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_TOKEN_LIB }} - - name: Setup Node.js (.npmrc) - uses: actions/setup-node@v3 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - node-version: 16.x - registry-url: https://npm.pkg.github.com/ - # Defaults to the user or organization that owns the workflow file + node-version: '20' + registry-url: 'https://npm.pkg.github.com/' scope: '@frmscoe' - - name: Set up NPM authentication - run: | - echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_TOKEN_LIB }}" > ~/.npmrc - cat .npmrc - - - name: Configure Git - run: | - git config user.email ${{ secrets.GH_EMAIL }} - git config user.name ${{ secrets.GH_USERNAME }} - - - name: Version bumping - env: - GH_TOKEN: '${{ secrets.GH_TOKEN }}' - run: | - commit_message=$(git log -1 --pretty=%B) - echo "Commit message: $commit_message" - if [[ "$commit_message" == *'feat!:'* ]]; then - npm version major - elif [[ "$commit_message" == *"feat:"* ]]; then - npm version minor - else - npm version prerelease --preid=rc - fi + - name: Set up npm authentication + run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_TOKEN_LIB }}" >> ~/.npmrc - name: Install dependencies run: npm ci env: - GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' + NODE_AUTH_TOKEN: ${{ secrets.GH_TOKEN_LIB }} - - name: Build library + - name: Build run: npm run build - name: Publish package - run: npm publish - env: - GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - NODE_AUTH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - - - name: Capture Version - id: capture_version run: | - export version=$(jq -r '.version' package.json) - echo "VERSION=$version" >> $GITHUB_ENV - - - name: Push Changes in package.json and make PRs - run: | - export GH_USERNAME=${{ secrets.GH_USERNAME }} - export GH_TOKEN=${{ secrets.GH_TOKEN_LIB }} - git config --global user.name ${{ secrets.GH_USERNAME }} - - # Clear the GITHUB_TOKEN environment variable and use a temporary file for gh authentication - echo "${{ secrets.GH_TOKEN_LIB }}" > /tmp/gh_token - unset GITHUB_TOKEN - unset GH_TOKEN - gh auth login --with-token < /tmp/gh_token - - git clone https://${{ secrets.GH_USERNAME }}:${{ secrets.GH_TOKEN_LIB }}@github.com/${{ github.repository }}.git - REPO_NAME=$(basename -s .git https://github.com/${{ github.repository }}.git) - cd $REPO_NAME - echo "Currently in repository directory: $(pwd)" - - if git ls-remote --heads origin version-bump | grep version-bump; then - # Branch exists, pull the latest changes - git checkout version-bump - git pull origin version-bump + VERSION=$(jq -r '.version' package.json) + if [[ "$VERSION" == *-* ]]; then + # Prerelease version (e.g. 1.2.3-rc.1): publish under the 'rc' dist-tag + # so it does not become the default 'latest' install target. + npm publish --tag rc else - # Branch does not exist, create it - git checkout -b version-bump + npm publish fi + env: + NODE_AUTH_TOKEN: ${{ secrets.GH_TOKEN_LIB }} - git config --global user.email ${{ secrets.GH_EMAIL }} - git config --global user.name ${{ secrets.GH_USERNAME }} - - # print current version - sed -i 's/"version": "[^"]*"/"version": "'"${{ env.VERSION }}"'"/' package.json - cat package.json - git add . - git commit -m "chore: Bump version after publishing to Github NPM" || echo "No changes to commit" - git push origin version-bump || git push origin version-bump --force - - gh pr create --title "build: Automated PR; Bump version after publishing to Github NPM" --body "This pull request updates the version in the `package.json` and `package-lock.json` after the package was published." --base dev --head version-bump --assignee ${{ secrets.GH_USERNAME }} --label build || echo "PR already exists, updating existing PR" - PR_ID=$(gh pr view --json number -q ".number") - echo "PR_ID=$pr_id" >> $GITHUB_ENV - - # Cleanup - rm /tmp/gh_token - - # Send Slack Notification - - name: Send Slack Notification + # Send Slack notification — requires SLACK_WEBHOOK_URL org/repo secret + - name: Send Slack notification env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} run: | - # Fetch the PR ID from the environment - PR_ID=${{ env.PR_ID }} - - curl -X POST -H 'Content-type: application/json' --data '{ + curl -s -X POST -H 'Content-type: application/json' --data '{ "blocks": [ { "type": "header", "text": { "type": "plain_text", - "text": "New NPM GitHub package published :white_check_mark:", + "text": "New npm package published :white_check_mark:", "emoji": true } }, @@ -142,20 +85,13 @@ jobs: "fields": [ { "type": "mrkdwn", - "text": "*Github Repository:*\nhttps://github.com/${{ github.repository }}" + "text": "*Repository:*\nhttps://github.com/${{ github.repository }}" }, { "type": "mrkdwn", - "text": "*Pull Requests:*\n" + "text": "*Triggered by:*\n${{ github.actor }}" } ] - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "Please head over to the github repository and merge the PR linked above to update the `package.json` with the newly published npm package." - } } ] - }' $SLACK_WEBHOOK_URL + }' "$SLACK_WEBHOOK_URL" || true diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml new file mode 100644 index 0000000..28c3403 --- /dev/null +++ b/.github/workflows/release-train.yml @@ -0,0 +1,261 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Prepares and opens a release PR from dev → main for a library package. +# +# Usage: trigger manually from the dev branch with the target stable version. +# - The version input MUST be a clean semver (e.g. "4.0.0") with no prerelease suffix. +# - The workflow resolves all internal rc dependencies to their stable equivalents, +# regenerates package-lock.json, commits via the GitHub API (producing a Verified +# commit that satisfies branch protection), and opens a PR → main for review. +# - After the PR is reviewed and merged, publish.yml fires automatically on push: main +# and publishes the package under the 'latest' dist-tag. +# +# Dependency resolution rules: +# Pinned rc (e.g. "7.0.2-rc.2") → replace with npm dist-tags.latest; fail if not stable +# Range with rc (e.g. "^4.0.0-rc.4") → strip prerelease from base → "^4.0.0"; fail if latest < base +# Pinned stable / range stable → leave unchanged +# +# Please do not attempt to edit this flow without the direct consent from the DevOps team. +# This file is managed centrally. + +name: Release train + +permissions: + contents: write + pull-requests: write + +# checkov:skip=CKV_GHA_7:Release train is operator-controlled; version input is validated and +# only determines the release branch name / package.json version — not the build artifact source. +on: + workflow_dispatch: + inputs: + version: + description: 'Target stable version (e.g. 4.0.0) — must not contain a prerelease suffix' + required: true + type: string + +jobs: + prepare-release: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + env: + VERSION_INPUT: ${{ inputs.version }} + + steps: + - name: Validate version input + run: | + VERSION="$VERSION_INPUT" + if [[ "$VERSION" == *-* ]]; then + echo "❌ Version input '$VERSION' contains a prerelease suffix." + echo " Provide a clean semver (e.g. 4.0.0), not a prerelease." + exit 1 + fi + # Basic semver format check: X.Y.Z + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ Version input '$VERSION' is not a valid semver (expected X.Y.Z)." + exit 1 + fi + echo "✅ Version input '$VERSION' is valid." + + - name: Checkout dev branch + uses: actions/checkout@v4 + with: + ref: dev + token: ${{ secrets.GH_TOKEN_LIB }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://npm.pkg.github.com/' + scope: '@frmscoe' + + - name: Add @tazama-lf registry scope + run: echo "@tazama-lf:registry=https://npm.pkg.github.com/" >> ~/.npmrc + + - name: Resolve internal rc dependencies to stable + id: resolve_deps + env: + NODE_AUTH_TOKEN: ${{ secrets.GH_TOKEN_LIB }} + run: | + CHANGED=0 + PKG=$(cat package.json) + + # Process both dependencies and devDependencies + for DEP_FIELD in dependencies devDependencies peerDependencies; do + # Get all @tazama-lf and @frmscoe keys in this field + KEYS=$(echo "$PKG" | jq -r ".${DEP_FIELD} // {} | keys[]" | grep -E '^@(tazama-lf|frmscoe)/' || true) + for KEY in $KEYS; do + CURRENT=$(echo "$PKG" | jq -r ".${DEP_FIELD}[\"${KEY}\"]") + echo "Checking $KEY = $CURRENT" + + # Detect pinned rc: X.Y.Z-rc.N (no leading ^ or ~) + if [[ "$CURRENT" =~ ^[0-9]+\.[0-9]+\.[0-9]+-[a-zA-Z] ]]; then + LATEST=$(npm view "${KEY}" dist-tags.latest 2>/dev/null || true) + if [ -z "$LATEST" ]; then + echo "❌ Cannot resolve stable version for ${KEY} (pinned rc: ${CURRENT})" + exit 1 + fi + if [[ "$LATEST" == *-* ]]; then + echo "❌ ${KEY}@latest is '${LATEST}' (still a prerelease). Publish stable first." + exit 1 + fi + echo " → $KEY: $CURRENT → $LATEST" + PKG=$(echo "$PKG" | jq ".${DEP_FIELD}[\"${KEY}\"] = \"${LATEST}\"") + CHANGED=1 + + # Detect range with rc prefix: ^X.Y.Z-rc.N or ~X.Y.Z-rc.N + elif [[ "$CURRENT" =~ ^[\^~][0-9]+\.[0-9]+\.[0-9]+-[a-zA-Z] ]]; then + RANGE_CHAR="${CURRENT:0:1}" + BASE="${CURRENT:1}" + STABLE_BASE="${BASE%%-*}" # strip -rc.N from base + LATEST=$(npm view "${KEY}" dist-tags.latest 2>/dev/null || true) + if [ -z "$LATEST" ]; then + echo "❌ Cannot resolve stable version for ${KEY} (range with rc: ${CURRENT})" + exit 1 + fi + if [[ "$LATEST" == *-* ]]; then + echo "❌ ${KEY}@latest is '${LATEST}' (still a prerelease). Publish stable first." + exit 1 + fi + NEW_RANGE="${RANGE_CHAR}${STABLE_BASE}" + echo " → $KEY: $CURRENT → $NEW_RANGE" + PKG=$(echo "$PKG" | jq ".${DEP_FIELD}[\"${KEY}\"] = \"${NEW_RANGE}\"") + CHANGED=1 + + else + echo " ✅ $KEY: $CURRENT (no change needed)" + fi + done + done + + # Write resolved package.json + echo "$PKG" | jq '.' > package.json + + echo "changed=$CHANGED" >> "$GITHUB_OUTPUT" + echo "Dependency resolution complete (changed=$CHANGED)" + + - name: Update version in package.json + run: | + VERSION="$VERSION_INPUT" + jq --arg v "$VERSION" '.version = $v' package.json > package.json.tmp + mv package.json.tmp package.json + echo "Version set to $VERSION" + + - name: Regenerate package-lock.json + run: npm install --package-lock-only + env: + NODE_AUTH_TOKEN: ${{ secrets.GH_TOKEN_LIB }} + + - name: Commit and push to release branch via GitHub API + id: push_branch + env: + GH_API_TOKEN: ${{ secrets.GH_TOKEN_LIB }} + run: | + VERSION="$VERSION_INPUT" + BRANCH="release/v${VERSION}" + REPO="${{ github.repository }}" + API="https://api.github.com/repos/${REPO}" + + # Get current dev branch SHA to base the new branch on + DEV_SHA=$(curl -s -H "Authorization: Bearer $GH_API_TOKEN" \ + "${API}/git/ref/heads/dev" | jq -r '.object.sha') + if [ -z "$DEV_SHA" ] || [ "$DEV_SHA" = "null" ]; then + echo "❌ Failed to get dev branch SHA — check API permissions" + exit 1 + fi + echo "Dev SHA: $DEV_SHA" + + # Create the release branch; if it already exists force-reset to current dev HEAD + # so that reruns always build from the latest dev state (not a stale branch tip) + CREATE_HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X POST -H "Authorization: Bearer $GH_API_TOKEN" \ + -H "Content-Type: application/json" \ + "${API}/git/refs" \ + -d "{\"ref\":\"refs/heads/${BRANCH}\",\"sha\":\"${DEV_SHA}\"}") + if [ "$CREATE_HTTP" = "201" ]; then + echo "Branch $BRANCH created at $DEV_SHA" + else + echo "Branch $BRANCH already exists — force-resetting to dev HEAD ($DEV_SHA)" + curl -s -X PATCH -H "Authorization: Bearer $GH_API_TOKEN" \ + -H "Content-Type: application/json" \ + "${API}/git/refs/heads/${BRANCH}" \ + -d "{\"sha\":\"${DEV_SHA}\",\"force\":true}" | jq -r '.ref // .message' + fi + + # Branch is now pinned to DEV_SHA in both cases + BRANCH_SHA="$DEV_SHA" + TREE_SHA=$(curl -s -H "Authorization: Bearer $GH_API_TOKEN" \ + "${API}/git/commits/${BRANCH_SHA}" | jq -r '.tree.sha') + if [ -z "$TREE_SHA" ] || [ "$TREE_SHA" = "null" ]; then + echo "❌ Failed to get tree SHA for commit $BRANCH_SHA" + exit 1 + fi + + # Build new tree with updated files + NEW_TREE=$(curl -s -X POST -H "Authorization: Bearer $GH_API_TOKEN" \ + -H "Content-Type: application/json" \ + "${API}/git/trees" \ + -d "{ + \"base_tree\": \"${TREE_SHA}\", + \"tree\": [ + {\"path\":\"package.json\",\"mode\":\"100644\",\"type\":\"blob\",\"content\":$(jq -Rs '.' package.json)}, + {\"path\":\"package-lock.json\",\"mode\":\"100644\",\"type\":\"blob\",\"content\":$(jq -Rs '.' package-lock.json)} + ] + }" | jq -r '.sha') + if [ -z "$NEW_TREE" ] || [ "$NEW_TREE" = "null" ]; then + echo "❌ Failed to create new tree via GitHub API" + exit 1 + fi + echo "New tree SHA: $NEW_TREE" + + # Create commit (GitHub API produces Verified commits) + NEW_COMMIT=$(curl -s -X POST -H "Authorization: Bearer $GH_API_TOKEN" \ + -H "Content-Type: application/json" \ + "${API}/git/commits" \ + -d "{ + \"message\": \"chore: prepare release v${VERSION}\n\n- Set version to ${VERSION}\n- Resolve internal rc dependencies to stable\n- Regenerate package-lock.json\", + \"tree\": \"${NEW_TREE}\", + \"parents\": [\"${BRANCH_SHA}\"] + }" | jq -r '.sha') + if [ -z "$NEW_COMMIT" ] || [ "$NEW_COMMIT" = "null" ]; then + echo "❌ Failed to create commit via GitHub API" + exit 1 + fi + echo "New commit SHA: $NEW_COMMIT" + + # Update the branch ref to the new commit + curl -s -X PATCH -H "Authorization: Bearer $GH_API_TOKEN" \ + -H "Content-Type: application/json" \ + "${API}/git/refs/heads/${BRANCH}" \ + -d "{\"sha\":\"${NEW_COMMIT}\"}" | jq -r '.ref // .message' + + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" + echo "Branch $BRANCH updated to commit $NEW_COMMIT" + + - name: Open PR to main + env: + GH_TOKEN: ${{ secrets.GH_TOKEN_LIB }} + run: | + VERSION="$VERSION_INPUT" + BRANCH="${{ steps.push_branch.outputs.branch }}" + + # Write PR body to temp file using printf to avoid heredoc/YAML conflicts + printf "## Release v%s\n\nThis PR was prepared automatically by release-train.yml.\n\n**Changes:**\n- Version bumped to %s\n- Internal rc dependencies resolved to stable\n- package-lock.json regenerated\n\n**After merging:**\n- publish.yml will fire on push to main and publish %s under the latest dist-tag.\n\nPlease review the dependency changes and version bump before approving.\n" \ + "$VERSION" "$VERSION" "$VERSION" > /tmp/pr-body.md + + if ! gh pr create \ + --title "release: v${VERSION}" \ + --body-file /tmp/pr-body.md \ + --base main \ + --head "$BRANCH" \ + --reviewer "${{ secrets.GH_USERNAME }}"; then + EXISTING=$(gh pr list --head "$BRANCH" --base main --json number --jq '.[0].number // empty') + if [ -n "$EXISTING" ]; then + echo "PR #${EXISTING} already exists for $BRANCH — skipping." + else + echo "❌ gh pr create failed and no existing PR was found." + exit 1 + fi + fi diff --git a/.github/workflows/sync-workflows.yml b/.github/workflows/sync-workflows.yml index 82e9faf..b712869 100644 --- a/.github/workflows/sync-workflows.yml +++ b/.github/workflows/sync-workflows.yml @@ -84,55 +84,93 @@ jobs: rule-084 rule-090 rule-091 - SPECIFIC_FILES: ${{ vars.SPECIFIC_FILES }} # List of specific files not to copy to certain repositories - SPECIFIC_REPOS: ${{ vars.SPECIFIC_REPOS }} # List of specific repositories needing specific files not included PR_REVIEWERS: ${{ vars.PR_REVIEWERS }} # List of reviewers GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} run: | SIGNED_OFF_BY="Signed-off-by: ${{ env.PR_AUTHOR_NAME_FULL }} <${{ env.PR_AUTHOR_EMAIL }}>" + + # Helper: check membership in a newline-delimited list + in_list() { echo "$2" | grep -Fxq "$1"; } + + # Build the temp-workflows bundle once, outside the repo loop + mkdir -p temp-workflows + cp -r .github/workflows/* temp-workflows/ + rm temp-workflows/sync-workflows.yml # avoid recursive syncing + rm -f temp-workflows/node.js.yml # preserve per-repo benchmark customisations + for repo in $REPOS; do git clone https://github.com/frmscoe/$repo.git cd $repo git remote set-url origin https://x-access-token:${{ secrets.GH_TOKEN }}@github.com/frmscoe/$repo.git if git ls-remote --heads origin sync-workflows-update | grep sync-workflows-update; then - # Branch exists, pull the latest changes git checkout sync-workflows-update git pull origin sync-workflows-update else - # Branch does not exist, create it git checkout -b sync-workflows-update fi cd .. - mkdir -p temp-workflows - cp -r .github/workflows/* temp-workflows/ - rm temp-workflows/sync-workflows.yml # Remove the sync-workflows.yml to avoid recursive syncing - rm temp-workflows/node.js.yml # Remove the node.js.yml to avoid removing benchmarking tests specific for each repo - mkdir -p $repo/.github/workflows # Create the workflows directory if it does not exist - - # Copy all files except the specific ones to certain repos - if [[ " ${SPECIFIC_REPOS} " =~ " ${repo} " ]]; then - for file in temp-workflows/*; do - if ! [[ " ${SPECIFIC_FILES} " =~ " $(basename $file) " ]]; then - cp -r $file $repo/.github/workflows/ - fi - done - else - cp -r temp-workflows/* $repo/.github/workflows/ - fi + mkdir -p $repo/.github/workflows + + # Copy all common workflow files; skip canonical package-rule*.yml definitions + # (rule repos receive caller stubs only — stamped below) + for file in temp-workflows/*; do + filename=$(basename "$file") + if [[ "${filename}" == package-rule*.yml ]]; then + continue + fi + cp -r "$file" "$repo/.github/workflows/" + done + + # Stamp caller stubs (replaces the full per-repo package-rule copies) + RULE_NUM="${repo#rule-}" # strip "rule-" prefix → "001", "044", etc. + # RC caller stub + printf '%s\n' \ + '# SPDX-License-Identifier: Apache-2.0' \ + '# This file is managed centrally — do not edit manually.' \ + '# To change build behaviour, update the reusable workflow in frmscoe/workflows.' \ + 'on:' \ + ' push:' \ + ' branches: [dev]' \ + ' workflow_dispatch:' \ + 'jobs:' \ + ' build:' \ + " uses: frmscoe/workflows/.github/workflows/package-rule-rc.yml@dev" \ + ' with:' \ + " rule_number: \"${RULE_NUM}\"" \ + ' rule_org: "frmscoe"' \ + ' secrets: inherit' \ + > "$repo/.github/workflows/package-rule-rc.yml" + # Stable caller stub + printf '%s\n' \ + '# SPDX-License-Identifier: Apache-2.0' \ + '# This file is managed centrally — do not edit manually.' \ + '# To change build behaviour, update the reusable workflow in frmscoe/workflows.' \ + 'on:' \ + ' push:' \ + ' branches: [main]' \ + ' workflow_dispatch:' \ + 'jobs:' \ + ' build:' \ + " uses: frmscoe/workflows/.github/workflows/package-rule.yml@main" \ + ' with:' \ + " rule_number: \"${RULE_NUM}\"" \ + ' rule_org: "frmscoe"' \ + ' secrets: inherit' \ + > "$repo/.github/workflows/package-rule.yml" + echo "Stamped caller stubs for rule-${RULE_NUM} (frmscoe)" cd $repo git add . git commit -m "ci: sync workflows from central-workflows ${SIGNED_OFF_BY}" || echo "No changes to commit" git push origin sync-workflows-update || git push origin sync-workflows-update --force - - # Clear the GITHUB_TOKEN environment variable and use a temporary file for gh authentication + + # Authenticate gh CLI with scoped token echo "${{ secrets.GH_TOKEN }}" > /tmp/gh_token unset GITHUB_TOKEN gh auth login --with-token < /tmp/gh_token - - # Create the PR with reviewers + IFS=',' read -ra REVIEWERS <<< "${PR_REVIEWERS}" REVIEWERS_ARGS="" for reviewer in "${REVIEWERS[@]}"; do @@ -140,9 +178,5 @@ jobs: done gh pr create --title "ci: sync workflows from central-workflows" --body "This PR syncs workflows from the central-workflows repository. ${SIGNED_OFF_BY}" --base dev --head sync-workflows-update $REVIEWERS_ARGS || echo "PR already exists, updating existing PR" - - # Cleanup + rm /tmp/gh_token - - cd .. - done diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000..5567037 --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: Apache-2.0 + +# Blocks a PR from merging to main if package.json contains a prerelease +# version suffix (e.g. -rc.1). The developer must strip the suffix manually +# in the release PR before this check will pass. +# +# This is the companion guard for the push: main trigger in publish.yml — +# it ensures that only clean semver versions (X.Y.Z) ever reach main and +# are published as the 'latest' dist-tag. +# +# Please do not attempt to edit this flow without the direct consent from the DevOps team. +# This file is managed centrally. + +name: Version check + +permissions: + contents: read + +on: + pull_request: + branches: + - main + +jobs: + check-version: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Reject prerelease version on main PR + run: | + VERSION=$(jq -r '.version' package.json) + if [[ "$VERSION" == *-* ]]; then + echo "❌ package.json version '$VERSION' contains a prerelease suffix." + echo " Strip the suffix (e.g. change '$VERSION' → '${VERSION%-*}') before merging to main." + exit 1 + fi + echo "✅ Version '$VERSION' is a clean release version — OK to merge." diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e0f3d36..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 - -## v1.1.0 (future date, 2024) - -* Next change summary here - -## v1.0.0 (July 2nd, 2024) - -* Add workflows, pr template, version and changelog.md file. \ No newline at end of file diff --git a/README.md b/README.md index edf98e1..b8c8919 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ # workflows -This repository is used for github workflows that will kick off Actions to be built before a PR can be pushed to the main branch. + +> **⚠️ This repository is a subordinate mirror of [tazama-lf/workflows](https://github.com/tazama-lf/workflows).** +> +> All workflow changes must be made in `tazama-lf/workflows` first. The `sync-workflows.yml` workflow in that repo propagates updates here automatically. **Do not edit workflow files in this repo directly** — any direct changes will be overwritten on the next sync. + +This repository holds the GitHub Actions workflows used by `frmscoe` organisation repositories. Workflows are gated on PRs to `dev` and `main`. diff --git a/VERSION b/VERSION deleted file mode 100644 index a20e049..0000000 --- a/VERSION +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 - -v1.0.0 \ No newline at end of file