From 8d6d6037f108fbb02e01a04a71c370d434e2ea5e Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 23:09:13 +0000 Subject: [PATCH 01/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:6a0b64892e93ab90d397a18099c1181b1ad9a607d3e14d086061dba47690b5be --- .github/inditextech-ci-sync-manifest.json | 30 +- .github/workflows/code-npm_node-PR_verify.yml | 6 +- ...-npm_node-publish-release-and-snapshot.yml | 174 ++++++++--- .../workflows/code-npm_node-release-core.yml | 278 ++++++++++++++---- .../code-npm_node-sonarcloud-analysis.yml | 13 +- .github/workflows/code-release_preview.yml | 15 +- .github/workflows/codeql.yml | 24 +- .github/workflows/push-verify.yml | 47 --- .github/workflows/scorecard-analysis.yml | 56 ++++ .github/workflows/sync-to-develop.yml | 25 +- 10 files changed, 467 insertions(+), 201 deletions(-) create mode 100644 .github/workflows/scorecard-analysis.yml diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json index 5ecd83d..934c5a2 100644 --- a/.github/inditextech-ci-sync-manifest.json +++ b/.github/inditextech-ci-sync-manifest.json @@ -13,7 +13,7 @@ "creation_year": 2026, "integrity": { "algorithm": "hmac-sha256", - "signature": "31eb5c1fe000ee6bda8bd74d8c1f273db26cdd9169be565b3bb9e03e36e9c137" + "signature": "edd8f10e850f84d223abcab60a5a6da6e0b00b541a99a650ce2bf70f0f14725b" }, "managed_by": "InditexTech CI governance", "managed_paths": { @@ -23,26 +23,34 @@ ".github/ISSUE_TEMPLATE/config.yml": "10e5dee4d49aa9e7792ceb4c986160c61f95da1ba00a64bf06e5f1f636cd17ec", ".github/PULL_REQUEST_TEMPLATE.md": "23a0b0ac79a9020ccac9c013582a01f86e2df2ae7f914673583b9a3368313041", ".github/inditextech-ci-node.json": "72449d1243d714b1ef9bd615e6dc67d0ec0b25f0c73440a08fa34d1e498864ac", - ".github/workflows/code-npm_node-PR_verify.yml": "29f72ba6e742fc56255f13d27ce650d6ccb2e184d9e2d9647e7c677220b90147", - ".github/workflows/code-npm_node-publish-release-and-snapshot.yml": "58b983b783aecbe835e7fad36f112ac8422dc0e83962cff32991001ebcc2d9ad", - ".github/workflows/code-npm_node-release-core.yml": "d4ec87242c201d99e0a12c42b7f17a0dda5d103759e1bea1620e7ed533232a5c", - ".github/workflows/code-npm_node-sonarcloud-analysis.yml": "8aedcf54db51ae185270ea059728c598caf1b14a5e19d4674180a4513821dd03", - ".github/workflows/code-release_preview.yml": "6222022149ea98df153afac623e1ecb5031f61607bcfda665058275e28c408f7", - ".github/workflows/codeql.yml": "301f1da228963e0586400b54ebdc7605e0a73c6443889cadc0fbde91869a52d6", + ".github/workflows/code-npm_node-PR_verify.yml": "89e11bc8aae9ec44ab47222cc570c27dffb8b426e88665598ea7065a9cc95d54", + ".github/workflows/code-npm_node-publish-release-and-snapshot.yml": "7ff9f7b0df30fca58db61e871d97654766e695fbebdf1e2d904da407964d35b0", + ".github/workflows/code-npm_node-release-core.yml": "1fd7d472a65b1dceefd86e1da878a356d211518951a55829ed1bd56b65bf1d82", + ".github/workflows/code-npm_node-sonarcloud-analysis.yml": "70c1c21825cde574d4744b308e198d030dbabddaa0f8ba07980be253ea6d7afa", + ".github/workflows/code-release_preview.yml": "4e2e95d60a498eb12d97ba5c8330b1173f04b02c807140beddad06a6e225b166", + ".github/workflows/codeql.yml": "633c0f288566fd1408e7ad545c7631e05f861ebf4ebdce3fe9586672ef065d97", ".github/workflows/pr-verify.yml": "f52295fbfe81578c0a6459d8b384be569933ac78d32fff370932a8dec6943e46", - ".github/workflows/push-verify.yml": "217097070d0fdef329eae1a7187806a4623d692ce467ca8f1f9a7f56dddb29ed", - ".github/workflows/sync-to-develop.yml": "9eb4278deb6be51157152068ccd5f1ee91866943ae9225fb9d8512aaacc88708", + ".github/workflows/push-verify.yml": "fd700ba0ee23fdee9cd7ecf23ad386247f4b621b73cc1946f9ef0eda4ea07086", + ".github/workflows/scorecard-analysis.yml": "5bd6da647f708cced5d33411cc27aac7f7ee2be06ae541defea677d70fe80481", + ".github/workflows/sync-to-develop.yml": "5c35cf0b946a939d0324b1530a78e319e979f993bd1f0105e64fc4478395d8d7", ".tool-versions": "5e7b05edf5d8df174df5dd99d012e57666ec9923ac8506df7dc7753ba41815fd", "CODE_OF_CONDUCT.md": "ce1e7a8f68a7917d48c03f9f7aae5529367f73af0e959276e889d33ea1e8d4ab", "CONTRIBUTING.md": "4e1264ca54a45df44b362c7533f0eba912bfa77b6f561121ae9e5e9d6aa00df3", "SECURITY.md": "0ee7a3356bc3a1c7649e3b7a6a9012b0608011be2b1ff8d106ad02c8950bab25", "repolinter.json": "0efb305c47a63f03c488a34906fc2d5471c84145c2ff553f2aacf3a41b92e1a1" }, + "managed_variables": { + "DEVELOPMENT_FLOW": "git-flow", + "PROJECT_TYPE": "single", + "PUBLISH_SNAPSHOT": "true", + "WORKING_DIRECTORY": "." + }, "profile": "node", "repository_id": 1291909277, + "retained_paths": {}, "schema_version": 2, "source_digests": { - "base": "8af88664d64bb62e112d3039ad780df69cc04f0b3fb5090fd12c8e72d8dab553", - "node": "f7151115162e66e928b2b690a5239bbb5c371eab2b2d6449cf019a787319969f" + "base": "5070fc83e3b1e67ddc4c2463e767ff3d0d364115c6433d2ae0de75afef2e0e63", + "node": "474a30f15f60cb8f19946fa26ac1cdfe97ed2f761ca6fdafa9e9cfdf26e44587" } } diff --git a/.github/workflows/code-npm_node-PR_verify.yml b/.github/workflows/code-npm_node-PR_verify.yml index abe7472..a9b5d66 100644 --- a/.github/workflows/code-npm_node-PR_verify.yml +++ b/.github/workflows/code-npm_node-PR_verify.yml @@ -39,7 +39,6 @@ jobs: shell: bash run: | set -euo pipefail - # Reject any .tool-versions line that is not a strict " " before asdf reads it (npm is pinned via package.json packageManager, not here). if grep -Evq '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$' .tool-versions; then echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." exit 1 @@ -52,7 +51,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Set up asdf-managed Node - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.0 + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 with: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} @@ -62,7 +61,6 @@ jobs: shell: bash run: | set -euo pipefail - # One install resolves the whole workspace; `verify` runs the governed lifecycle (lint, unit tests, build) across the tree. case "$PROJECT_TYPE" in single|workspaces) : ;; *) @@ -70,14 +68,12 @@ jobs: exit 1 ;; esac - # Package manager defaults to npm; an absent PACKAGE_MANAGER var renders the npm path unchanged (byte-identical to the pre-pnpm template). case "${PACKAGE_MANAGER:-npm}" in npm) npm ci npm run verify ;; pnpm) - # Corepack activates the exact pnpm pinned in the product's package.json "packageManager" field: no floating pnpm, no product edit. corepack enable pnpm install --frozen-lockfile pnpm run verify diff --git a/.github/workflows/code-npm_node-publish-release-and-snapshot.yml b/.github/workflows/code-npm_node-publish-release-and-snapshot.yml index 0fa05ba..5085f20 100644 --- a/.github/workflows/code-npm_node-publish-release-and-snapshot.yml +++ b/.github/workflows/code-npm_node-publish-release-and-snapshot.yml @@ -4,9 +4,7 @@ name: code-npm-node-publish-release-and-snapshot run-name: Publish npm (release or snapshot) from ${{ github.event_name }} -# Single registrable top-level npm publish entrypoint (Topology B): npm trusted publishing keys on the workflow FILENAME, so the privileged `npm publish` runs INLINE here (never in a called reusable workflow) and both `workflow_ref` and `job_workflow_ref` resolve to this one file; snapshot and release lanes are separate jobs that hand a pre-built tarball to the pinned gh-actions/npm composite ACTION inside a reviewer-gated Environment, and the unprivileged build stage is the only one running candidate code. on: - # workflow_dispatch drives the RELEASE lane (it carries release_type); the snapshot lane fires on push / issue_comment only, so a manual dispatch never cuts a snapshot. workflow_dispatch: inputs: release_type: @@ -32,7 +30,6 @@ permissions: contents: read concurrency: - # Lane-separated group key so snapshot and release runs never share a group: snapshot (push / issue_comment) is cancel-in-progress, release (pull_request / workflow_dispatch) is not. group: >- ${{ format('code-npm-node-publish-{0}-{1}', github.repository, (github.event_name == 'push' || github.event_name == 'issue_comment') @@ -48,9 +45,6 @@ env: SNAPSHOT_ARTIFACT: npm-snapshot-dist jobs: - # Snapshot lane (jobs: authorize -> build -> publish). - - # Unprivileged pre-gate for /publish-snapshot: a cheap read-only filter, not a substitute for the reviewer-gated npm-snapshot Environment; resolves the PR head so the build stage checks out the reviewed commit. authorize: name: Authorize snapshot command if: >- @@ -73,18 +67,22 @@ jobs: shell: bash run: | set -euo pipefail - # Read-only lookup of the PR head SHA; issue_comment carries the number, not the head commit. - head_sha="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '.head.sha')" + head="$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq '[.head.sha, .head.repo.full_name] | @tsv')" + head_sha="$(printf '%s' "$head" | cut -f1)" + head_repo="$(printf '%s' "$head" | cut -f2)" if [[ -z "$head_sha" || "$head_sha" == "null" ]]; then echo "::error title=Unresolved head::Could not resolve the pull request head commit." >&2 exit 1 fi + if [[ "$head_repo" != "$GITHUB_REPOSITORY" ]]; then + echo "::error title=Fork head rejected::Snapshot publishing only accepts branches of ${GITHUB_REPOSITORY}; refusing head from ${head_repo}." >&2 + exit 1 + fi echo "head_sha=${head_sha}" >> "$GITHUB_OUTPUT" build: name: Build snapshot needs: [authorize] - # Snapshot builds fire on the trusted release line (push) or an authorized /publish-snapshot comment only; workflow_dispatch is reserved for releases. if: >- !cancelled() && ( @@ -122,13 +120,11 @@ jobs: shell: bash run: | set -euo pipefail - # Snapshot publishing is opt-in (descriptor `publish_snapshot`, default off); an absent PUBLISH_SNAPSHOT variable disables the deploy on both the push and comment lanes. if [[ "${PUBLISH_SNAPSHOT:-false}" != "true" ]]; then echo "::notice title=Snapshot disabled::publish_snapshot opt-in is off; no snapshot is published." echo "should_publish=false" >> "$GITHUB_OUTPUT" exit 0 fi - # The next-development commit already restores -SNAPSHOT, so skip it; key on the release-bot committer identity `[bot]@users.noreply.github.com` rather than the commit subject, which the project owns in delegated mode. head_committer_email="$(git show -s --format=%ce HEAD)" if [[ "$GITHUB_EVENT_NAME" == "push" && "$head_committer_email" == *"[bot]@users.noreply.github.com" ]]; then echo "::notice title=Snapshot skipped::Next-dev commit does not need a snapshot." @@ -144,7 +140,6 @@ jobs: shell: bash run: | set -euo pipefail - # Reject any .tool-versions line that is not a strict " " before asdf reads it (npm is pinned via package.json packageManager, not here). if grep -Evq '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$' .tool-versions; then echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." exit 1 @@ -158,7 +153,7 @@ jobs: - name: Set up asdf-managed Node if: steps.plan.outputs.should_publish == 'true' - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.0 + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 with: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} @@ -175,22 +170,19 @@ jobs: set -euo pipefail strip_snapshot() { printf '%s' "${1%-SNAPSHOT}"; } - compute_snapshot() { # -> snapshot version for this run + compute_snapshot() { if [[ "$GITHUB_EVENT_NAME" == "issue_comment" ]]; then - # On-demand PR-head snapshot: encode the PR number so concurrent PRs never collide on the `next` dist-tag. printf '%s-PR%s-SNAPSHOT.%s' "$1" "$PR_NUMBER" "$GITHUB_RUN_NUMBER" else - # Trusted release-line snapshot: run number + attempt keep every re-run monotonic and unique. printf '%s-SNAPSHOT.%s.%s' "$1" "$GITHUB_RUN_NUMBER" "$GITHUB_RUN_ATTEMPT" fi } - member_dir() { # -> flat workspaces member directory + member_dir() { printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' } mkdir -p "$DIST_DIR" - # Install, build, stamp the run-unique snapshot version per unit (no lifecycle scripts), then pack the pre-built tree; npm packs workspaces in one call while pnpm packs each member from its own dir, and `npm pkg set` stamps versions in both paths without touching any lockfile. case "${PACKAGE_MANAGER:-npm}" in npm) npm ci @@ -217,7 +209,6 @@ jobs: esac ;; pnpm) - # Corepack activates the exact pnpm pinned in the product's package.json "packageManager" field: no floating pnpm, no product edit. corepack enable pnpm install --frozen-lockfile pnpm run build @@ -259,11 +250,9 @@ jobs: if-no-files-found: error retention-days: 5 - # Publish stage: no candidate code runs here. The privileged npm publish is INLINE in this top-level file (so OIDC job_workflow_ref == the registered workflow_ref), granting id-token: write only at this reviewer-gated npm-snapshot boundary to ship the pre-built tarball. publish: name: Publish snapshot needs: [build] - # Deploy is gated on the descriptor snapshot opt-in (belt-and-suspenders with the Plan step's should_publish): the credential-bearing deploy never runs unless publish_snapshot is enabled. if: >- !cancelled() && needs.build.outputs.should_publish == 'true' && @@ -273,13 +262,11 @@ jobs: environment: npm-snapshot permissions: contents: read - id-token: write # The npm trusted-publishing OIDC boundary, inline in the registered top-level file. + id-token: write env: - # Environment-scoped fallback for a package not yet a registered trusted publisher; set at JOB level so the composite's run steps inherit it, and unreachable from the build stage. NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Check out trusted publish ref - # Its own trusted ref, never the candidate head; the reviewed bytes ship as the pre-built artifact only. uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -291,27 +278,22 @@ jobs: path: ${{ runner.temp }}/publish-dist - name: Publish via npm trusted publishing - # The composite publishes each pre-built tarball with --ignore-scripts, sets the dist-tag, enables provenance only when the repository is public, and falls back to NPM_TOKEN for an unregistered package. - uses: InditexTech/gh-actions/npm@5159a0213e29a091bf732553681a0de8ac712d17 # gh-actions/npm composite @ main; no release tag yet + uses: InditexTech/gh-actions/npm@977030536dbdb52a7fe2fd5979510fd6a225d035 # v1.1.0 with: working-directory: ${{ vars.WORKING_DIRECTORY }} project-type: ${{ vars.PROJECT_TYPE }} dist-tag: next artifact-directory: ${{ runner.temp }}/publish-dist - # NPM_TOKEN is provided at job level: a composite's run steps do not inherit a caller step's env. - - # Release lane (jobs: release-core -> publish-npm -> github-release). release-core: name: Prepare and Build Release - # Lane routing: the release cut runs only on release triggers, never on a push / comment (those drive the snapshot lane above). if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' uses: ./.github/workflows/code-npm_node-release-core.yml with: release_type: ${{ inputs.release_type }} - secrets: inherit + secrets: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} - # Govern-inject publish path: release-core packed the tarballs and THIS top-level file publishes them INLINE (so OIDC job_workflow_ref == the registered workflow_ref) through the pinned gh-actions/npm composite, holding id-token: write only in this reviewer-gated boundary; skipped in delegated mode. publish-npm: name: Publish ${{ matrix.release.tag }} to npm needs: release-core @@ -321,17 +303,15 @@ jobs: environment: npm-registry permissions: contents: read - id-token: write # The npm trusted-publishing OIDC boundary, inline in the registered top-level file. + id-token: write strategy: fail-fast: false matrix: release: ${{ fromJSON(needs.release-core.outputs.releases) }} env: - # Environment-scoped fallback for a package not yet a registered trusted publisher; set at JOB level so the composite's run steps inherit it. NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Check out trusted publish ref - # Its own trusted ref, never a candidate head; the release bytes ship as the pre-built artifact only. uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -343,16 +323,13 @@ jobs: path: ${{ runner.temp }}/publish-dist - name: Publish via npm trusted publishing - # The composite publishes each pre-built tarball with --ignore-scripts, sets the dist-tag, enables provenance only when the repository is public, and falls back to NPM_TOKEN for an unregistered package. - uses: InditexTech/gh-actions/npm@5159a0213e29a091bf732553681a0de8ac712d17 # gh-actions/npm composite @ main; no release tag yet + uses: InditexTech/gh-actions/npm@977030536dbdb52a7fe2fd5979510fd6a225d035 # v1.1.0 with: working-directory: ${{ vars.WORKING_DIRECTORY }} project-type: ${{ vars.PROJECT_TYPE }} dist-tag: latest artifact-directory: ${{ runner.temp }}/publish-dist - # NPM_TOKEN is provided at job level: a composite's run steps do not inherit a caller step's env. - # Delegated publish path: the project's own release:perform runs under the reviewer-gated npm-registry Environment with the ENV-TOKEN (NPM_TOKEN), NEVER id-token, and is trusted-trigger-only so it carries no pwn-request surface; the inline publish-npm job above stays the SOLE id-token boundary, preserving the single-registrable-entrypoint invariant. publish-npm-delegated: name: Publish delegated release to npm needs: release-core @@ -389,44 +366,145 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Set up asdf-managed Node - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.0 + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 with: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} - name: Install dependencies - # No publish credential in scope here, so dependency install scripts never see NPM_TOKEN; the token is added only to the release:perform step below. working-directory: ${{ env.WORKING_DIRECTORY }} shell: bash run: | set -euo pipefail - # Package manager defaults to npm; pnpm is provisioned via corepack from the product's packageManager pin. case "${PACKAGE_MANAGER:-npm}" in - npm) npm ci ;; - pnpm) corepack enable; pnpm install --frozen-lockfile ;; + npm) npm ci --ignore-scripts ;; + pnpm) corepack enable; pnpm install --frozen-lockfile --ignore-scripts ;; *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; esac - name: Run project release:perform working-directory: ${{ env.WORKING_DIRECTORY }} env: - # Reviewer-gated Environment secret and the sole publish credential in the delegated lane; no id-token is minted here. NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} shell: bash run: | set -euo pipefail - # The release commit already passed verify and its tags are pushed; the project's release:perform publishes the tagged versions through its own package manager (corepack enabled above) and owns the publish shape. case "${PACKAGE_MANAGER:-npm}" in npm) npm run release:perform ;; pnpm) pnpm run release:perform ;; *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; esac + finalize-delegated-release: + name: Finalize delegated release (next dev + sync PR) + needs: [release-core, publish-npm-delegated] + if: >- + vars.RELEASE_LIFECYCLE == 'delegated' && + needs.release-core.result == 'success' && + needs.publish-npm-delegated.result == 'success' + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + steps: + - name: Create GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - name: Check out release branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.base.ref || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Read governed tool versions + id: tool-versions + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + if grep -Evq '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$' .tool-versions; then + echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." + exit 1 + fi + { + echo "tool_versions<> "$GITHUB_OUTPUT" + + - name: Set up asdf-managed Node + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 + with: + tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} + asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + + - name: Next development iteration and sync PR + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + BASELINE_BRANCH: ${{ github.event.pull_request.base.ref || github.ref_name }} + DEVELOPMENT_FLOW: ${{ vars.DEVELOPMENT_FLOW }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASES: ${{ needs.release-core.outputs.releases }} + RELEASE_COMMIT: ${{ needs.release-core.outputs.release_commit }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + gh auth setup-git + git config user.name "${APP_SLUG}[bot]" + git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + + if [[ "$(git rev-parse HEAD)" != "$RELEASE_COMMIT" ]]; then + echo "::notice title=Next-dev skipped::Branch already advanced past the release commit." + else + release_bump="$(jq -r '.[0].release_bump // ""' <<< "$RELEASES")" + case "${PACKAGE_MANAGER:-npm}" in + npm) RELEASE_BUMP="$release_bump" npm run version:development ;; + pnpm) corepack enable; RELEASE_BUMP="$release_bump" pnpm run version:development ;; + *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; + esac + git -C "$GITHUB_WORKSPACE" add --update -- "$WORKING_DIRECTORY" + if ! git diff --cached --quiet; then + git commit -m "[node-release] prepare for next development iteration" + fi + git push origin "HEAD:refs/heads/$BASELINE_BRANCH" + fi + + if [[ "$DEVELOPMENT_FLOW" != "git-flow" ]]; then + echo "::notice title=Sync skipped::Sync-to-develop applies only to git-flow." + exit 0 + fi + + version="$(jq -r '.[0].version' <<< "$RELEASES")" + develop_branch="${BASELINE_BRANCH/main/develop}" + if [[ -z "$(git ls-remote --heads origin "$develop_branch")" ]]; then + echo "::notice title=Sync skipped::$develop_branch does not exist." + exit 0 + fi + sync_branch="automated/sync-release-${version}-to-${develop_branch}" + git switch --force-create "$sync_branch" "origin/$BASELINE_BRANCH" + git push --force-with-lease --set-upstream origin "$sync_branch" + + if [[ -z "$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$sync_branch" --base "$develop_branch" --state open --json number --jq '.[0].number // empty')" ]]; then + gh pr create --repo "$GITHUB_REPOSITORY" \ + --base "$develop_branch" --head "$sync_branch" \ + --title "Sync release $version to $develop_branch" \ + --body "**Automated pull request** syncing the release cut on \`$BASELINE_BRANCH\` back into \`$develop_branch\`." + fi + github-release: name: Create GitHub Release ${{ matrix.release.tag }} needs: [release-core, publish-npm, publish-npm-delegated] - # Tolerate whichever publish path was skipped for the active lifecycle. if: >- !cancelled() && needs.release-core.result == 'success' && @@ -450,7 +528,6 @@ jobs: permission-contents: write - name: Download release distributions - # Govern-inject only: the engine packed tarballs to attach; in delegated mode release:perform published directly, so the Release carries generated notes only. if: vars.RELEASE_LIFECYCLE != 'delegated' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -458,7 +535,6 @@ jobs: path: dist - name: Create or update GitHub Release - # release-core already created and pushed the annotated tag; this attaches the packed tarballs in govern-inject and is notes-only in delegated, with --clobber keeping re-runs idempotent. env: GH_TOKEN: ${{ steps.app-token.outputs.token }} RELEASE_TAG: ${{ matrix.release.tag }} diff --git a/.github/workflows/code-npm_node-release-core.yml b/.github/workflows/code-npm_node-release-core.yml index 3783033..b27a7eb 100644 --- a/.github/workflows/code-npm_node-release-core.yml +++ b/.github/workflows/code-npm_node-release-core.yml @@ -18,19 +18,23 @@ on: releases: description: JSON array of release tags, versions, package lists, and next-dev versions. value: ${{ jobs.prepare-release.outputs.releases }} + secrets: + APP_PRIVATE_KEY: + required: true permissions: contents: read env: + CI_GOVERNANCE_RELEASE_STAGING_REF: refs/ci-governance/release/${{ github.run_id }}-${{ github.run_attempt }} PROJECT_TYPE: ${{ vars.PROJECT_TYPE }} WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} PACKAGE_MANAGER: ${{ vars.PACKAGE_MANAGER }} ASDF_BRANCH_VERSION: '0.18.0' jobs: - prepare-release: - name: Prepare Release + verify-candidate: + name: Verify release candidate if: >- ( github.event_name == 'workflow_dispatch' && @@ -42,6 +46,7 @@ jobs: ( github.event_name == 'pull_request' && github.event.pull_request.merged && + github.event.pull_request.head.ref != 'automated/ci-governance-sync' && !contains(join(github.event.pull_request.labels.*.name, ','), 'skip-release') && ( (vars.DEVELOPMENT_FLOW == 'trunk-based-development' && (github.event.pull_request.base.ref == 'main' || startsWith(github.event.pull_request.base.ref, 'main-'))) || @@ -54,9 +59,120 @@ jobs: ) runs-on: ubuntu-24.04 timeout-minutes: 30 + permissions: + contents: read + steps: + - name: Resolve release context + id: release-context + env: + EVENT_SHA: ${{ github.sha }} + PR_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + shell: bash + run: | + set -euo pipefail + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + release_ref="$EVENT_SHA" + else + release_ref="$PR_MERGE_SHA" + fi + if [[ ! "$release_ref" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error title=Invalid release context::An immutable release commit is required." + exit 1 + fi + echo "release_ref=$release_ref" >> "$GITHUB_OUTPUT" + + - name: Check out release candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ steps.release-context.outputs.release_ref }} + + - name: Read governed tool versions + id: tool-versions + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + if grep -Evq '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$' .tool-versions; then + echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." + exit 1 + fi + { + echo "tool_versions<> "$GITHUB_OUTPUT" + + - name: Set up asdf-managed Node + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 + with: + tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} + asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + + - name: Verify release candidate + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + case "$PROJECT_TYPE" in + single|workspaces) : ;; + *) + echo "::error title=Invalid project type::PROJECT_TYPE must be single or workspaces." + exit 1 + ;; + esac + case "${PACKAGE_MANAGER:-npm}" in + npm) + npm ci + npm run verify + ;; + pnpm) + corepack enable + pnpm install --frozen-lockfile + pnpm run verify + ;; + *) + echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm." + exit 1 + ;; + esac + + prepare-release: + name: Prepare Release + needs: verify-candidate + if: >- + needs.verify-candidate.result == 'success' && + ( + ( + github.event_name == 'workflow_dispatch' && + ( + (vars.DEVELOPMENT_FLOW == 'trunk-based-development' && (github.ref_name == 'main' || startsWith(github.ref_name, 'main-'))) || + (vars.DEVELOPMENT_FLOW == 'git-flow' && (github.ref_name == 'main' || startsWith(github.ref_name, 'main-'))) + ) + ) || + ( + github.event_name == 'pull_request' && + github.event.pull_request.merged && + github.event.pull_request.head.ref != 'automated/ci-governance-sync' && + !contains(join(github.event.pull_request.labels.*.name, ','), 'skip-release') && + ( + (vars.DEVELOPMENT_FLOW == 'trunk-based-development' && (github.event.pull_request.base.ref == 'main' || startsWith(github.event.pull_request.base.ref, 'main-'))) || + (vars.DEVELOPMENT_FLOW == 'git-flow' && (github.event.pull_request.base.ref == 'main' || startsWith(github.event.pull_request.base.ref, 'main-'))) + ) && + ( + contains(join(github.event.pull_request.labels.*.name, ','), 'release-type') || + vars.DEVELOPMENT_FLOW == 'trunk-based-development' + ) + ) + ) + runs-on: ubuntu-24.04 + timeout-minutes: 30 outputs: release_commit: ${{ steps.release-metadata.outputs.release_commit }} releases: ${{ steps.release-metadata.outputs.releases }} + expected_ref: ${{ steps.release-plan.outputs.expected_ref }} + staging_ref: ${{ steps.release-metadata.outputs.staging_ref }} steps: - name: Resolve release context id: release-context @@ -91,6 +207,19 @@ jobs: echo "release_ref=$release_ref" } >> "$GITHUB_OUTPUT" + - name: Enforce protected release branch + env: + GH_TOKEN: ${{ github.token }} + BASELINE_BRANCH: ${{ steps.release-context.outputs.baseline_branch }} + shell: bash + run: | + set -euo pipefail + protected="$(gh api "repos/${GITHUB_REPOSITORY}/branches/${BASELINE_BRANCH}" --jq '.protected')" + if [[ "$protected" != "true" ]]; then + echo "::error title=Unprotected release branch::Releases require a protected baseline branch; '${BASELINE_BRANCH}' is not protected." >&2 + exit 1 + fi + - name: Create GitHub App token id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 @@ -126,37 +255,21 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Set up asdf-managed Node - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.0 + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 with: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} - - name: Verify release candidate + - name: Install dependencies for delegated release + if: vars.RELEASE_LIFECYCLE == 'delegated' working-directory: ${{ env.WORKING_DIRECTORY }} shell: bash run: | set -euo pipefail - case "$PROJECT_TYPE" in - single|workspaces) : ;; - *) - echo "::error title=Invalid project type::PROJECT_TYPE must be single or workspaces." - exit 1 - ;; - esac case "${PACKAGE_MANAGER:-npm}" in - npm) - npm ci - npm run verify - ;; - pnpm) - corepack enable - pnpm install --frozen-lockfile - pnpm run verify - ;; - *) - echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm." - exit 1 - ;; + npm) npm ci --ignore-scripts ;; + pnpm) corepack enable; pnpm install --frozen-lockfile --ignore-scripts ;; + *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; esac - name: Plan release metadata @@ -231,7 +344,7 @@ jobs: jq -r '.version' package.json fi } - bump_semver() { # + bump_semver() { local major minor patch IFS=. read -r major minor patch <<< "$1" case "$2" in @@ -334,7 +447,6 @@ jobs: set_version "$(jq -r '.[0].version' <<< "$releases")" "" fi - # Reconcile workspace cross-dependency specs to each bumped sibling's released version and regenerate the lockfile so published metadata resolves the bumped siblings, not the pre-release -SNAPSHOT specs (needs bash>=4 declare -A; ubuntu runner). if [[ "$PROJECT_TYPE" == "workspaces" ]]; then declare -A member_version=() while IFS= read -r record; do @@ -438,7 +550,7 @@ jobs: rm -f "$tmp" fi - - name: Commit, tag and push release + - name: Commit and stage release if: vars.RELEASE_LIFECYCLE != 'delegated' && steps.release-plan.outputs.resume != 'true' id: release-commit env: @@ -460,16 +572,11 @@ jobs: git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" git commit -m "[node-release] Prepare release" - tag_refs=() while IFS= read -r record; do - tag="$(jq -r '.tag' <<< "$record")" - version="$(jq -r '.version' <<< "$record")" - git check-ref-format --allow-onelevel "refs/tags/$tag" - git tag -a "$tag" -m "Release $version" - tag_refs+=("refs/tags/$tag") + git check-ref-format --allow-onelevel "refs/tags/$(jq -r '.tag' <<< "$record")" done < <(jq -c '.[]' <<< "$RELEASES") - git push --atomic origin "HEAD:$EXPECTED_REF" "${tag_refs[@]}" + git push --atomic --force origin "HEAD:$CI_GOVERNANCE_RELEASE_STAGING_REF" - name: Cut delegated release id: delegated-plan @@ -595,6 +702,18 @@ jobs: rm -f "$tmp" fi + # Lifecycle trade-off, stated on purpose. The delegated lane publishes the + # baseline branch and the cut tags HERE, before anything has been built or + # published, because the project owns its release shape: `release:perform` + # runs later, in the caller, and is handed a commit whose tags are already + # on the remote. The engine therefore CANNOT make a delegated node release + # atomic -- a failed `release:perform` leaves the version consumed, and the + # only recovery is to cut the next one. The govern-inject lane stages first + # and promotes after the build precisely so it does not have this property. + # Restoring atomicity here would mean deferring this push until after + # `publish-npm-delegated`, which lives in the workflow that CALLS this + # reusable one; a job here cannot depend on a job there. See + # `engine/docs/agent-rules/node-adapter.md`. - name: Push delegated release id: delegated-push if: vars.RELEASE_LIFECYCLE == 'delegated' @@ -637,12 +756,18 @@ jobs: id: release-metadata env: RELEASES: ${{ steps.release-plan.outputs.releases_intermediate || steps.delegated-plan.outputs.releases_intermediate }} + RELEASE_LIFECYCLE: ${{ vars.RELEASE_LIFECYCLE }} working-directory: ${{ env.WORKING_DIRECTORY }} shell: bash run: | set -euo pipefail + staging_ref="$CI_GOVERNANCE_RELEASE_STAGING_REF" + if [[ "$RELEASE_LIFECYCLE" == "delegated" ]]; then + staging_ref="" + fi { echo "release_commit=$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" + echo "staging_ref=$staging_ref" echo "releases<> "$GITHUB_OUTPUT" - name: Set up asdf-managed Node - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.0 + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 with: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} @@ -760,6 +885,7 @@ jobs: needs: [prepare-release, build-distributions] if: >- !cancelled() && + vars.RELEASE_LIFECYCLE != 'delegated' && needs.prepare-release.result == 'success' && (needs.build-distributions.result == 'success' || needs.build-distributions.result == 'skipped') runs-on: ubuntu-24.04 @@ -802,12 +928,59 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Set up asdf-managed Node - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.0 + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 with: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + - name: Promote the release to the default branch + id: promote + if: vars.RELEASE_LIFECYCLE != 'delegated' && (needs.build-distributions.result == 'success' || needs.build-distributions.result == 'skipped') + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + EXPECTED_REF: ${{ needs.prepare-release.outputs.expected_ref }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASES: ${{ needs.prepare-release.outputs.releases }} + RELEASE_COMMIT: ${{ needs.prepare-release.outputs.release_commit }} + STAGING_REF: ${{ needs.prepare-release.outputs.staging_ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + gh auth setup-git + git config user.name "${APP_SLUG}[bot]" + git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + + git fetch --no-tags origin "+${STAGING_REF}:refs/ci-governance/staged" 2>/dev/null || true + if [[ "$(git ls-remote origin "$EXPECTED_REF" | cut -f1)" == "$RELEASE_COMMIT" ]]; then + echo "::notice title=Promotion skipped::$EXPECTED_REF already points at the release commit." + git checkout --force --detach "$RELEASE_COMMIT" + exit 0 + fi + + if [[ "$(git rev-parse --verify --quiet refs/ci-governance/staged || true)" != "$RELEASE_COMMIT" ]]; then + echo "::error title=Release staging mismatch::$STAGING_REF does not point at the release commit." + exit 1 + fi + git checkout --force --detach "$RELEASE_COMMIT" + + tag_refs=() + while IFS= read -r record; do + tag="$(jq -r '.tag' <<< "$record")" + version="$(jq -r '.version' <<< "$record")" + git check-ref-format --allow-onelevel "refs/tags/$tag" + if git ls-remote --exit-code --tags origin "refs/tags/$tag" >/dev/null 2>&1; then + echo "::error title=Tag conflict::$tag already exists." + exit 1 + fi + git tag -a "$tag" -m "Release $version" + tag_refs+=("refs/tags/$tag") + done < <(jq -c '.[]' <<< "$RELEASES") + + git push --atomic origin "${RELEASE_COMMIT}:${EXPECTED_REF}" "${tag_refs[@]}" + - name: Next development iteration and sync PR + if: steps.promote.outcome == 'success' env: APP_SLUG: ${{ steps.app-token.outputs.app-slug }} BASELINE_BRANCH: ${{ github.event.pull_request.base.ref || github.ref_name }} @@ -815,7 +988,6 @@ jobs: GH_TOKEN: ${{ steps.app-token.outputs.token }} RELEASES: ${{ needs.prepare-release.outputs.releases }} RELEASE_COMMIT: ${{ needs.prepare-release.outputs.release_commit }} - RELEASE_LIFECYCLE: ${{ vars.RELEASE_LIFECYCLE }} working-directory: ${{ env.WORKING_DIRECTORY }} shell: bash run: | @@ -833,18 +1005,7 @@ jobs: if [[ "$(git rev-parse HEAD)" != "$RELEASE_COMMIT" ]]; then echo "::notice title=Next-dev skipped::Branch already advanced past the release commit." else - if [[ "$RELEASE_LIFECYCLE" == "delegated" ]]; then - release_bump="$(jq -r '.[0].release_bump // ""' <<< "$RELEASES")" - case "${PACKAGE_MANAGER:-npm}" in - npm) RELEASE_BUMP="$release_bump" npm run version:development ;; - pnpm) corepack enable; RELEASE_BUMP="$release_bump" pnpm run version:development ;; - *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; - esac - git -C "$GITHUB_WORKSPACE" add --update -- "$WORKING_DIRECTORY" - if ! git diff --cached --quiet; then - git commit -m "[node-release] prepare for next development iteration" - fi - else + { if [[ "$PROJECT_TYPE" == "single" ]]; then next_dev="$(jq -r '.[0].next_dev' <<< "$RELEASES")" npm pkg set "version=$next_dev" @@ -862,7 +1023,6 @@ jobs: done < <(jq -c '.[]' <<< "$RELEASES") fi - # Same cross-dependency reconciliation as the release strip, on the next-dev bump: point each sibling spec at the bumped member's next_dev and regenerate the lockfile, or main keeps cross-deps at the released version with a stale lockfile and `npm ci` fails (needs bash>=4). if [[ "$PROJECT_TYPE" == "workspaces" ]]; then declare -A member_version=() while IFS= read -r record; do @@ -894,7 +1054,7 @@ jobs: git -C "$GITHUB_WORKSPACE" add --update -- "$WORKING_DIRECTORY" git commit -m "[node-release] prepare for next development iteration" - fi + } git push origin "HEAD:refs/heads/$BASELINE_BRANCH" fi @@ -919,3 +1079,19 @@ jobs: --title "Sync release $version to $develop_branch" \ --body "**Automated pull request** syncing the release cut on \`$BASELINE_BRANCH\` back into \`$develop_branch\`." fi + + - name: Discard the release staging ref + if: always() + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + STAGING_REF: ${{ needs.prepare-release.outputs.staging_ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${STAGING_REF:-}" ]]; then + echo "::notice title=Nothing staged::No staging ref was written, so there is nothing to discard." + exit 0 + fi + gh auth setup-git + git push origin ":${STAGING_REF}" diff --git a/.github/workflows/code-npm_node-sonarcloud-analysis.yml b/.github/workflows/code-npm_node-sonarcloud-analysis.yml index f4eda71..8325255 100644 --- a/.github/workflows/code-npm_node-sonarcloud-analysis.yml +++ b/.github/workflows/code-npm_node-sonarcloud-analysis.yml @@ -63,7 +63,6 @@ jobs: shell: bash run: | set -euo pipefail - # Reject any .tool-versions line that is not a strict " " before asdf reads it (npm is pinned via package.json packageManager, not here). if grep -Evq '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$' .tool-versions; then echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." exit 1 @@ -76,7 +75,7 @@ jobs: } >> "$GITHUB_OUTPUT" - name: Set up asdf-managed Node - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.0 + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 with: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} @@ -86,7 +85,6 @@ jobs: shell: bash run: | set -euo pipefail - # `test` is the governed lifecycle script and must emit the lcov report the scanner reads; build runs first so the scanner sees buildable sources. case "$PROJECT_TYPE" in single|workspaces) npm ci @@ -103,7 +101,6 @@ jobs: shell: bash run: | set -euo pipefail - # Resolve sonar.sources and lcov coverage paths from the repository root; a workspaces build contributes every member so none is silently excluded. case "$PROJECT_TYPE" in single) sonar_sources="${WORKING_DIRECTORY}" @@ -114,12 +111,10 @@ jobs: coverage=() for member_dir in "${WORKING_DIRECTORY}"/packages/*/; do member_dir="${member_dir%/}" - # Only members with a package.json are analyzable units; skip stray directories under packages/. if [[ ! -f "${member_dir}/package.json" ]]; then continue fi sources+=("${member_dir}") - # Attribute each member's coverage from its own lcov report; the scanner ignores a path a member without tests never wrote. coverage+=("${member_dir}/coverage/lcov.info") done if [[ "${#sources[@]}" -eq 0 ]]; then @@ -134,6 +129,12 @@ jobs: exit 1 ;; esac + for governed_value in "$sonar_sources" "$sonar_coverage"; do + if [[ "$governed_value" == *$'\n'* ]]; then + echo "::error title=Invalid Sonar scope::Resolved analysis paths must not contain newlines." >&2 + exit 1 + fi + done { echo "SONAR_SOURCES=${sonar_sources}" echo "SONAR_COVERAGE_PATHS=${sonar_coverage}" diff --git a/.github/workflows/code-release_preview.yml b/.github/workflows/code-release_preview.yml index a1d9a97..30de715 100644 --- a/.github/workflows/code-release_preview.yml +++ b/.github/workflows/code-release_preview.yml @@ -33,7 +33,7 @@ jobs: timeout-minutes: 15 permissions: contents: read - pull-requests: write # Publishes the calculated release preview. + pull-requests: write steps: - name: Check out pull request head uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -43,10 +43,11 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Check CHANGELOG has Unreleased changes - # Skip for PRs that opt out of a release (skip-release label) and for the engine's automated reconcile PR, which never carries release content. if: >- !contains(github.event.pull_request.labels.*.name, 'skip-release') && - github.event.pull_request.head.ref != 'automated/ci-governance-sync' + github.event.pull_request.head.ref != 'automated/ci-governance-sync' && + github.event.pull_request.user.login != 'dependabot[bot]' && + github.event.pull_request.user.login != 'renovate[bot]' env: GH_TOKEN: ${{ github.token }} GITHUB_EVENT_NUMBER: ${{ github.event.pull_request.number }} @@ -104,18 +105,16 @@ jobs: merge_strategy="Squash and merge" fi - # npm version helpers (mirror release-core): release version is strip-SNAPSHOT of each unit's package.json; the release-type label only drives the next-dev bump. - member_dir() { # -> flat workspaces member directory - # @scope/api -> api, client_lib -> client-lib (mirrors the CREATE scaffolder). + member_dir() { printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' } strip_snapshot() { printf '%s' "${1%-SNAPSHOT}"; } - read_version() { # + read_version() { local manifest="package.json" [[ -n "$1" ]] && manifest="$1/package.json" jq -r '.version' "$manifest" } - bump_semver() { # + bump_semver() { local major minor patch IFS=. read -r major minor patch <<< "$1" case "$2" in diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d855c42..0ec0eb2 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -21,7 +21,6 @@ concurrency: cancel-in-progress: true jobs: - # Resolves code-scanning availability at runtime so the analysis skips cleanly (green) where no Advanced Security seat is licensed, and starts automatically once a repository becomes public or gains a seat. scanning-availability: name: Resolve code scanning availability if: >- @@ -44,7 +43,7 @@ jobs: ) runs-on: ubuntu-24.04 permissions: - contents: read # Reads repository metadata to resolve availability. + contents: read outputs: enabled: ${{ steps.resolve.outputs.enabled }} steps: @@ -52,21 +51,19 @@ jobs: id: resolve env: GH_TOKEN: ${{ github.token }} - # Manual opt-in (CODE_SCANNING_ENABLED='true') for a seated private/internal repository whose seat status the workflow token cannot read. FORCE_ENABLED: ${{ vars.CODE_SCANNING_ENABLED }} run: | set -euo pipefail - # Visibility is always readable; the security-and-analysis block is admin-token-only, so treat a missing value as unknown. visibility="$(gh api "/repos/${GITHUB_REPOSITORY}" --jq '.visibility' 2>/dev/null || echo unknown)" seat="$(gh api "/repos/${GITHUB_REPOSITORY}" \ --jq '.security_and_analysis.advanced_security.status // "unknown"' 2>/dev/null || echo unknown)" enabled=false if [ "${visibility}" = "public" ]; then - enabled=true # Code scanning is always free and available on public repositories. + enabled=true elif [ "${seat}" = "enabled" ]; then - enabled=true # A Code Security seat is attached, so analysis is licensed. + enabled=true elif [ "${FORCE_ENABLED:-}" = "true" ]; then - enabled=true # Operator opted this repository in explicitly. + enabled=true fi echo "Code scanning availability: visibility=${visibility} seat=${seat} enabled=${enabled}" echo "enabled=${enabled}" >> "${GITHUB_OUTPUT}" @@ -77,17 +74,16 @@ jobs: if: needs.scanning-availability.outputs.enabled == 'true' runs-on: ubuntu-24.04 permissions: - actions: read # Lets CodeQL inspect workflow metadata. + actions: read contents: read - packages: read # Lets CodeQL resolve package metadata during analysis. - security-events: write # Uploads CodeQL results to Code Scanning. + packages: read + security-events: write strategy: fail-fast: false matrix: include: - language: actions build-mode: none - # JS/TS is analyzed buildless: CodeQL's extractor reads source directly, so no Node/npm setup or workspace build is required. - language: javascript-typescript build-mode: none steps: @@ -101,13 +97,9 @@ jobs: with: build-mode: ${{ matrix.build-mode }} languages: ${{ matrix.language }} - # These three `actions` queries are false positives on the governed lanes: CodeQL propagates a caller's issue_comment/workflow_dispatch triggers through `workflow_call` and cannot evaluate the job-level `if:` guards, yet every flagged site is mitigated in-template (40-hex merge-commit SHA validation, whitelisted bump labels, no PR-head code in privileged jobs); scoped to these rule IDs only. + # Release caches are branch-scoped, so a release-core build can never write the default-branch cache. config: | query-filters: - - exclude: - id: actions/untrusted-checkout/critical - - exclude: - id: actions/envvar-injection/critical - exclude: id: actions/cache-poisoning/poisonable-step diff --git a/.github/workflows/push-verify.yml b/.github/workflows/push-verify.yml index 6f0c7b0..b5c40f9 100644 --- a/.github/workflows/push-verify.yml +++ b/.github/workflows/push-verify.yml @@ -35,53 +35,6 @@ jobs: reporter: github-check fail_level: error - scorecard-analysis: - if: >- - github.ref_type == 'branch' && - github.event.repository.visibility == 'public' && - github.ref_name == github.event.repository.default_branch - name: Scorecard analysis - runs-on: ubuntu-24.04 - timeout-minutes: 20 - permissions: - contents: read - id-token: write # Lets Scorecard prove the published result's provenance. - security-events: write # Uploads the generated Scorecard SARIF to Code Scanning. - steps: - - name: "Checkout code" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: "Run analysis" - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 - with: - results_file: results.sarif - results_format: sarif - # Scorecard team runs a weekly scan of public GitHub repos, - # see https://github.com/ossf/scorecard#public-data. - # Setting `publish_results: true` helps us scale by leveraging your workflow to - # extract the results instead of relying on our own infrastructure to run scans. - # And it's free for you! - publish_results: true - - # Upload the results as artifacts (optional). Commenting out will disable - # uploads of run results in SARIF format to the repository Actions tab. - # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts - - name: "Upload artifact" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - - name: Upload SARIF to Code Scanning - uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 - with: - sarif_file: results.sarif - category: scorecard - repo-linter: if: >- github.ref_type == 'branch' && diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml new file mode 100644 index 0000000..7ee0523 --- /dev/null +++ b/.github/workflows/scorecard-analysis.yml @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: Scorecard analysis + +permissions: + contents: read + +on: + push: + +concurrency: + group: scorecard-analysis-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + scorecard-analysis: + if: >- + github.ref_type == 'branch' && + github.ref_name == github.event.repository.default_branch + name: Scorecard analysis + runs-on: ubuntu-24.04 + timeout-minutes: 20 + permissions: + contents: read + issues: read # Lets Scorecard read issue metadata via the GraphQL API. + pull-requests: read # Lets Scorecard read pull-request metadata via the GraphQL API. + id-token: write # Lets Scorecard prove the published result's provenance. + security-events: write # Uploads the generated Scorecard SARIF to Code Scanning. + actions: read # Lets the SARIF upload read workflow run metadata on non-public repositories. + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + publish_results: ${{ github.event.repository.visibility == 'public' }} + + - name: Upload artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + - name: Upload SARIF to Code Scanning + uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + with: + sarif_file: results.sarif + category: scorecard diff --git a/.github/workflows/sync-to-develop.yml b/.github/workflows/sync-to-develop.yml index a9ea870..de7c21a 100644 --- a/.github/workflows/sync-to-develop.yml +++ b/.github/workflows/sync-to-develop.yml @@ -28,6 +28,20 @@ jobs: group: sync-to-develop-${{ github.repository }}-${{ github.event.pull_request.base.ref }} cancel-in-progress: false steps: + # The checkout runs *before* any App credential exists. This event carries + # the base repository's secrets, so ordering is the cheap half of the + # defence: content that lands in the workspace cannot reach a credential + # that has not been minted yet. + - name: Checkout the exact merged revision + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + # Deliberately the base branch, not the merge SHA. Central policy + # requires this ref, and the branch is trusted here: this event only + # fires for an already-merged pull request. + ref: ${{ github.event.pull_request.base.ref }} + - name: Create GitHub App token id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 @@ -37,16 +51,11 @@ jobs: permission-contents: write permission-issues: write permission-pull-requests: write + # Required because the synchronization branch carries workflow files + # from the default branch into develop, and Git refuses that push + # without it. permission-workflows: write - - name: Checkout base branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ github.event.pull_request.base.ref }} - token: ${{ steps.app-token.outputs.token }} - - name: Prepare sync branch id: prepare env: From b47487fefd874965ff21cdcacb4078777feefeff Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:25:03 +0000 Subject: [PATCH 02/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:0f249eadee667c0720b4428085c2911ac844a40ca6699bb7bd2d635c37f508c2 From 45f26aea9dbebf31d58ec073212ba22f41124968 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:00:28 +0000 Subject: [PATCH 03/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:ea6b346fbb4c9145afdcf7381bfa8d4650f03e00d9145e90dfac7718507cb623 From 273601eb1b9ec611e968ebc3249d9445ee9a99cd Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 01:31:12 +0000 Subject: [PATCH 04/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:dbaff2d86dbd94834139d5ab659620762a1352172ad0bc9e00fdcad200cb2f16 From 56bc7efac9a419a3a1a6c6822ab060da9f875c89 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:06:52 +0000 Subject: [PATCH 05/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:12e528431d8d134a935801b12b7f041c2706e0e28c4bc49fc61b9dabdc9c9249 From e3e96d02c1c93e30c3f838cf92750e52542c78f9 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:15:35 +0000 Subject: [PATCH 06/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:2f0d7f5425a45e0f21a18ff1a32a837c03d3865ff75a0ff5ceb374b025404cb3 From 5a44314c99662751883cd9c78e761a8d1b7ae3ba Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:37:56 +0000 Subject: [PATCH 07/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:bac626773b03bea4a0c63ea286c73797d2c563f467e165daba0a19f9155ca66e From 99182c4dfd4e43b17f01ce1e07017d6e34b1e8b8 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 02:55:32 +0000 Subject: [PATCH 08/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:eee01a36ca67295ba596653c9766ad6d7dd705e580020ace84bc4a7ff482c27f From 8d511a87ceb9fe0bcced3f38027fd2090ffd85f9 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 03:11:24 +0000 Subject: [PATCH 09/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:b4dbb328ab7ec030ca56d57bd2816c1b5115bee99ccbdb49ad70ad3e1c0f5168 From eeecc35daa3a83e7840c82bdbd81277dcff1b2b0 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 03:16:52 +0000 Subject: [PATCH 10/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:c6284d206edfca47e4d112ef1c836abcdfffb749cb673a7a81cb3da42052e5fd From de6b1903bd5cf5af725d779ca0560f0b4dde4bce Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 03:31:12 +0000 Subject: [PATCH 11/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:9ee85cd3ce3c701db6722fab09be77b13d9d41b078d3a91ffeb7ff288ab7ed8e From c15c8379a99753a57892abc32e4807579f7b8dc5 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 04:30:40 +0000 Subject: [PATCH 12/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:9ae7a5325c38a4a076f0bd9aa52b33504fdfb5b7d32943d912b10a1ae9acb541 From 9b9aba5a6acf9b21e04866b84e664b65f065fdeb Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:39:02 +0000 Subject: [PATCH 13/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:3fa171644c5b407912ac29fa0f1583cc1d1dd7e44dc2d17f14760c7ed029c435 From 3fc40fa89026cfe89f559827651c0edb607c63fb Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 09:26:07 +0000 Subject: [PATCH 14/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a7e40cb361a91263a40f5311d8d9a0b9d253f49240f9238357e1e88444d791cf From fb2b6265a6225ec76c3d96ad862e89a6da532028 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 10:26:44 +0000 Subject: [PATCH 15/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:cc78e1b1411389bcedb842813a5da3c535cd2ca77025935ed726297685c5bdba From 2c6bc0c99ffdc51fbf48dc4aad4483aab34e10d1 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:11:56 +0000 Subject: [PATCH 16/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:2c58225cd57a1bfa0dc7227dd30f72982e3a68fe855a0ae12eca63b11cdbb25a From 79b8b158ba1f0f872302cf07e14bb8ea554d72a7 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:20:34 +0000 Subject: [PATCH 17/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:ebdceb5827218b4d3e4e098194b6e9e7bf45c57a1544690014047c4830e1f190 From 991c1db4ddeac3176e8ed773936f2ab3db629a39 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:26:01 +0000 Subject: [PATCH 18/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:5d588131e47a7db19ff20431915c4346604706cf54e6b6c19a08c545839da68f From 88854c738835c2fef16d4f0fc6d1142c60f6d6f5 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:42:41 +0000 Subject: [PATCH 19/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:33eab9508fed75b7e15a7c2a5734533f327dd179141d09bb7004f88314dbf514 From fa1a3c4e136ba14c99669b758e60d8ffce4b9bb9 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:02:11 +0000 Subject: [PATCH 20/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:e0c4b6190d648dd69eccac2f07bd9fce1f99828cc48186b0d39d99eacd21b610 From 0e2651b63a6f9948d6e973d8ca9aff4e6a9bf015 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:35:38 +0000 Subject: [PATCH 21/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:22994287f494e3d1efc507fbd23c6cca88d4d526c404be1e27c94460b6fe2071 From 5ccce21ca5d5ea1c9a4670161b699025eb1763a3 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:27:22 +0000 Subject: [PATCH 22/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:dc870901e75b20dbd5f2294d79d29ec68949a6ec97a579121fc6baa59cf894d3 From d68c14c0469e2b8a49279385b2857fde093d2c69 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:42:07 +0000 Subject: [PATCH 23/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:cb51e8a5dabc2a283092a24f11a7835e17637268ebfc8675507030cb3fc008e3 From 0862923bfc77a0f4ebad92f2bc01b6080999492a Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:47:46 +0000 Subject: [PATCH 24/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:dd28b9fc9405ce5b751154c875350e7602430af80c87d8ef6294ab84be60e9df From fd59370de4ebfd707f82cbb5cbe3b35c796f1b51 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 21:53:52 +0000 Subject: [PATCH 25/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:1c1d7ea97fe8a0ed0fcfe9027f3c7255616156087a610f2d693160c97e80c281 From 081b15d01a1d09070e10efdaba9ee473b96acdeb Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:06:10 +0000 Subject: [PATCH 26/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:bb9c9884744f37661dcc59c1a4c028028c03ee38b626567218ac2a5d439920c6 From 112c95b2b869395e45de5d99b84d6d22f11fdfa9 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:21:32 +0000 Subject: [PATCH 27/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:3d56c645f18349e2160b0bd669e084885c628b3f78e4aea31e71189abc880ff6 From 3532b132d7b4a73c5a2635d1eb69ba668ae4bb7c Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:54:28 +0000 Subject: [PATCH 28/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:be408a2af28b27e3294c7f9b78e9a5087f3321ebda24141b93954d5f45008b6c From 60c056bead4495d15393fc2a322ce231e5f49b96 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:59:33 +0000 Subject: [PATCH 29/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:b435440ae34b0eadf96b33b1ec155ee10c41143b453bc74773372cbb63d6ffa6 From a9e81ba47f22105d895433011c2c8da5640fe65c Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 00:16:40 +0000 Subject: [PATCH 30/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:08efe558df969867a4adea670ee478c7c6883ec2e7c0a9037fb3d5f7a77dcf37 From dad35a617a9146414b891ca4bee0f0c4b748a36e Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:13:19 +0000 Subject: [PATCH 31/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:92f9bc5b1b3dd7522e85862acaaa2f4d36ca77196a73c87b4d861ad474816085 --- .github/inditextech-ci-sync-manifest.json | 8 +- ...-npm_node-publish-release-and-snapshot.yml | 436 +++++++++++++++--- .../workflows/code-npm_node-release-core.yml | 393 ++++++++++++++-- 3 files changed, 722 insertions(+), 115 deletions(-) diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json index 934c5a2..765382e 100644 --- a/.github/inditextech-ci-sync-manifest.json +++ b/.github/inditextech-ci-sync-manifest.json @@ -13,7 +13,7 @@ "creation_year": 2026, "integrity": { "algorithm": "hmac-sha256", - "signature": "edd8f10e850f84d223abcab60a5a6da6e0b00b541a99a650ce2bf70f0f14725b" + "signature": "6cbdc2ef01327a5d5d9bc3da231434bb01b83dc5878f319398798c12c3904c5e" }, "managed_by": "InditexTech CI governance", "managed_paths": { @@ -24,8 +24,8 @@ ".github/PULL_REQUEST_TEMPLATE.md": "23a0b0ac79a9020ccac9c013582a01f86e2df2ae7f914673583b9a3368313041", ".github/inditextech-ci-node.json": "72449d1243d714b1ef9bd615e6dc67d0ec0b25f0c73440a08fa34d1e498864ac", ".github/workflows/code-npm_node-PR_verify.yml": "89e11bc8aae9ec44ab47222cc570c27dffb8b426e88665598ea7065a9cc95d54", - ".github/workflows/code-npm_node-publish-release-and-snapshot.yml": "7ff9f7b0df30fca58db61e871d97654766e695fbebdf1e2d904da407964d35b0", - ".github/workflows/code-npm_node-release-core.yml": "1fd7d472a65b1dceefd86e1da878a356d211518951a55829ed1bd56b65bf1d82", + ".github/workflows/code-npm_node-publish-release-and-snapshot.yml": "878a28f8b8042411ccd8123a95aca6132c083a2d2da7fd0d10095997c9678764", + ".github/workflows/code-npm_node-release-core.yml": "3b409b668d51f67bb683b56905d314fd807de32f4f4c46555ce9de4cb3bc3c73", ".github/workflows/code-npm_node-sonarcloud-analysis.yml": "70c1c21825cde574d4744b308e198d030dbabddaa0f8ba07980be253ea6d7afa", ".github/workflows/code-release_preview.yml": "4e2e95d60a498eb12d97ba5c8330b1173f04b02c807140beddad06a6e225b166", ".github/workflows/codeql.yml": "633c0f288566fd1408e7ad545c7631e05f861ebf4ebdce3fe9586672ef065d97", @@ -51,6 +51,6 @@ "schema_version": 2, "source_digests": { "base": "5070fc83e3b1e67ddc4c2463e767ff3d0d364115c6433d2ae0de75afef2e0e63", - "node": "474a30f15f60cb8f19946fa26ac1cdfe97ed2f761ca6fdafa9e9cfdf26e44587" + "node": "d2f8082132a512a1770518d0995acf54caa0690a8e0fea20b5691805c6a5473c" } } diff --git a/.github/workflows/code-npm_node-publish-release-and-snapshot.yml b/.github/workflows/code-npm_node-publish-release-and-snapshot.yml index 5085f20..d39af6f 100644 --- a/.github/workflows/code-npm_node-publish-release-and-snapshot.yml +++ b/.github/workflows/code-npm_node-publish-release-and-snapshot.yml @@ -263,8 +263,6 @@ jobs: permissions: contents: read id-token: write - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Check out trusted publish ref uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -279,6 +277,8 @@ jobs: - name: Publish via npm trusted publishing uses: InditexTech/gh-actions/npm@977030536dbdb52a7fe2fd5979510fd6a225d035 # v1.1.0 + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} with: working-directory: ${{ vars.WORKING_DIRECTORY }} project-type: ${{ vars.PROJECT_TYPE }} @@ -308,8 +308,6 @@ jobs: fail-fast: false matrix: release: ${{ fromJSON(needs.release-core.outputs.releases) }} - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - name: Check out trusted publish ref uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -324,6 +322,8 @@ jobs: - name: Publish via npm trusted publishing uses: InditexTech/gh-actions/npm@977030536dbdb52a7fe2fd5979510fd6a225d035 # v1.1.0 + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} with: working-directory: ${{ vars.WORKING_DIRECTORY }} project-type: ${{ vars.PROJECT_TYPE }} @@ -331,26 +331,276 @@ jobs: artifact-directory: ${{ runner.temp }}/publish-dist publish-npm-delegated: - name: Publish delegated release to npm + name: Promote and publish delegated release to npm needs: release-core if: >- vars.RELEASE_LIFECYCLE == 'delegated' && + needs.release-core.result == 'success' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') runs-on: ubuntu-24.04 timeout-minutes: 30 environment: npm-registry permissions: contents: read + id-token: write steps: - - name: Check out release commit + - name: Download immutable delegated release handoff + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: node-delegated-release-handoff + path: ${{ runner.temp }}/node-delegated-release-handoff + + - name: Verify delegated release handoff + id: handoff + env: + HANDOFF_DIR: ${{ runner.temp }}/node-delegated-release-handoff + shell: bash + run: | + set -euo pipefail + metadata="$HANDOFF_DIR/release-handoff.json" + source_bundle="$HANDOFF_DIR/release-source.bundle" + publish_dist="${RUNNER_TEMP}/publish-dist" + if [[ ! -f "$metadata" || ! -f "$source_bundle" || ! -d "$HANDOFF_DIR/tarballs" ]]; then + echo "::error title=Incomplete release handoff::The immutable source, metadata, and tarball directory are all required." + exit 1 + fi + if [[ "$(sha256sum "$metadata" | awk '{print $1}')" != "${{ needs.release-core.outputs.handoff_digest }}" ]]; then + echo "::error title=Tampered release handoff::The run-bound release metadata digest does not match preparation." + exit 1 + fi + jq -e \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ + ' + .schema_version == 1 and + .repository == $repository and + .run_id == $run_id and + .run_attempt == $run_attempt and + (.expected_ref | test("^refs/heads/[A-Za-z0-9._/-]+$")) and + (.source_commit | test("^[0-9a-f]{40}$")) and + (.release_commit | test("^[0-9a-f]{40}$")) and + (.source_bundle.file == "release-source.bundle") and + (.source_bundle.sha256 | test("^[0-9a-f]{64}$")) and + (.releases | type == "array" and length > 0) and + (.tags | type == "array" and length > 0) and + (.tarballs | type == "array" and length > 0) + ' "$metadata" > /dev/null + if [[ "$(sha256sum "$source_bundle" | awk '{print $1}')" != "$(jq -r '.source_bundle.sha256' "$metadata")" ]]; then + echo "::error title=Tampered release handoff::The release source bundle digest does not match metadata." + exit 1 + fi + jq -e ' + (.tags | all(.[]; (.name | type == "string" and length > 0) and (.target | type == "string" and test("^[0-9a-f]{40}$")))) and + (.releases | all(.[]; (.tag | type == "string" and length > 0) and (.version | type == "string" and length > 0))) and + (([.tags[].name] | sort) == ([.releases[].tag] | sort)) and + (.tarballs | all(.[]; (.file | type == "string" and test("^[A-Za-z0-9][A-Za-z0-9._-]*\\.tgz$")) and (.package | type == "string" and length > 0) and (.release_tag | type == "string" and length > 0) and (.version | type == "string" and length > 0) and (.sha256 | type == "string" and test("^[0-9a-f]{64}$")))) + ' "$metadata" > /dev/null + + release_tag_for() { + local package_name="$1" + local package_version="$2" + local member record release_tag release_version + local -a exact_matches=() + local -a version_matches=() + member="$(printf '%s' "${package_name##*/}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" + while IFS= read -r record; do + release_tag="$(jq -r '.tag' <<< "$record")" + release_version="$(jq -r '.version' <<< "$record")" + [[ "$release_version" == "$package_version" ]] || continue + version_matches+=("$release_tag") + if [[ "$release_tag" == "$package_version" || "$release_tag" == "${member}-${package_version}" || "$release_tag" == "${package_name}-${package_version}" ]]; then + exact_matches+=("$release_tag") + fi + done < <(jq -c '.releases[]' "$metadata") + if [[ ${#exact_matches[@]} -eq 1 ]]; then + printf '%s' "${exact_matches[0]}" + elif [[ ${#exact_matches[@]} -eq 0 && ${#version_matches[@]} -eq 1 ]]; then + printf '%s' "${version_matches[0]}" + else + echo "::error title=Ambiguous package release::Package '$package_name@$package_version' does not resolve to exactly one prepared release tag." >&2 + exit 1 + fi + } + + rm -rf "$HANDOFF_DIR/source" "$publish_dist" + git clone --quiet --no-checkout "$source_bundle" "$HANDOFF_DIR/source" + source_commit="$(jq -r '.source_commit' "$metadata")" + release_commit="$(jq -r '.release_commit' "$metadata")" + git -C "$HANDOFF_DIR/source" rev-parse --verify --quiet "${source_commit}^{commit}" > /dev/null + git -C "$HANDOFF_DIR/source" rev-parse --verify --quiet "${release_commit}^{commit}" > /dev/null + if ! git -C "$HANDOFF_DIR/source" merge-base --is-ancestor "$source_commit" "$release_commit"; then + echo "::error title=Invalid release provenance::The prepared release is not descended from the verified source commit." + exit 1 + fi + + tag_count="$(jq -r '.tags | length' "$metadata")" + unique_tag_count="$(jq -r '[.tags[].name] | unique | length' "$metadata")" + if [[ "$unique_tag_count" -ne "$tag_count" ]]; then + echo "::error title=Invalid release tags::The handoff repeats a release tag." + exit 1 + fi + while IFS= read -r tag; do + git check-ref-format --allow-onelevel "refs/tags/$tag" + target="$(jq -er --arg tag "$tag" '.tags[] | select(.name == $tag) | .target | select(test("^[0-9a-f]{40}$"))' "$metadata")" + if [[ "$(git -C "$HANDOFF_DIR/source" rev-parse "${tag}^{commit}")" != "$target" ]]; then + echo "::error title=Invalid release tag::Prepared tag '$tag' does not resolve to its recorded commit." + exit 1 + fi + if ! git -C "$HANDOFF_DIR/source" merge-base --is-ancestor "$source_commit" "$target" \ + || ! git -C "$HANDOFF_DIR/source" merge-base --is-ancestor "$target" "$release_commit"; then + echo "::error title=Invalid release tag provenance::Prepared tag '$tag' must satisfy source <= tag <= release." + exit 1 + fi + done < <(jq -r '.tags[].name' "$metadata") + + mkdir -p "$publish_dist" + tarball_count="$(jq -r '.tarballs | length' "$metadata")" + unique_tarball_count="$(jq -r '[.tarballs[].file] | unique | length' "$metadata")" + if [[ "$unique_tarball_count" -ne "$tarball_count" ]]; then + echo "::error title=Invalid package artifacts::The handoff repeats a tarball filename." + exit 1 + fi + while IFS= read -r tarball_file; do + tarball="$HANDOFF_DIR/tarballs/$tarball_file" + expected_sha="$(jq -er --arg file "$tarball_file" '.tarballs[] | select(.file == $file) | .sha256' "$metadata")" + expected_package="$(jq -er --arg file "$tarball_file" '.tarballs[] | select(.file == $file) | .package' "$metadata")" + expected_release_tag="$(jq -er --arg file "$tarball_file" '.tarballs[] | select(.file == $file) | .release_tag' "$metadata")" + expected_version="$(jq -er --arg file "$tarball_file" '.tarballs[] | select(.file == $file) | .version' "$metadata")" + if [[ ! -f "$tarball" ]] || [[ "$(sha256sum "$tarball" | awk '{print $1}')" != "$expected_sha" ]]; then + echo "::error title=Tampered package artifact::Tarball '$tarball_file' is missing or does not match the prepared digest." + exit 1 + fi + tar -tzf "$tarball" | grep -qx 'package/package.json' + if [[ "$(tar -xzOf "$tarball" package/package.json | jq -r '.name')" != "$expected_package" ]] \ + || [[ "$(tar -xzOf "$tarball" package/package.json | jq -r '.version')" != "$expected_version" ]]; then + echo "::error title=Tampered package artifact::Tarball '$tarball_file' package identity does not match the prepared metadata." + exit 1 + fi + mapped_release_tag="$(release_tag_for "$expected_package" "$expected_version")" + if [[ "$mapped_release_tag" != "$expected_release_tag" ]] \ + || [[ "$(jq -r --arg tag "$expected_release_tag" --arg version "$expected_version" '[.releases[] | select(.tag == $tag and .version == $version)] | length' "$metadata")" -ne 1 ]]; then + echo "::error title=Invalid package release::Tarball '$expected_package@$expected_version' is not bound to exactly one prepared release tag." + exit 1 + fi + cp "$tarball" "$publish_dist/$tarball_file" + done < <(jq -r '.tarballs[].file' "$metadata") + if [[ "$(find "$publish_dist" -maxdepth 1 -type f -name '*.tgz' | wc -l | tr -d ' ')" -ne "$tarball_count" ]]; then + echo "::error title=Incomplete package artifacts::The protected publish directory does not contain the complete verified tarball set." + exit 1 + fi + + { + echo "expected_ref=$(jq -r '.expected_ref' "$metadata")" + echo "release_commit=$release_commit" + echo "source_commit=$source_commit" + } >> "$GITHUB_OUTPUT" + + - name: Create GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permission-contents: write + + - name: Check out trusted release baseline uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + fetch-depth: 0 persist-credentials: false - ref: ${{ needs.release-core.outputs.release_commit }} + ref: ${{ github.event.pull_request.base.ref || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + + - name: Promote verified delegated release refs + env: + EXPECTED_REF: ${{ steps.handoff.outputs.expected_ref }} + HANDOFF_DIR: ${{ runner.temp }}/node-delegated-release-handoff + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASE_COMMIT: ${{ steps.handoff.outputs.release_commit }} + SOURCE_COMMIT: ${{ steps.handoff.outputs.source_commit }} + shell: bash + run: | + set -euo pipefail + gh auth setup-git + source="$HANDOFF_DIR/source" + git -C "$source" remote set-url origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" + if [[ "$(git ls-remote origin "$EXPECTED_REF" | cut -f1)" != "$SOURCE_COMMIT" ]]; then + echo "::error title=Release baseline moved::The protected release branch no longer points to the verified source commit." + exit 1 + fi + tag_refs=() + while IFS= read -r tag; do + if git ls-remote --exit-code --tags origin "refs/tags/$tag" > /dev/null 2>&1; then + echo "::error title=Tag conflict::Release tag '$tag' already exists on the remote." + exit 1 + fi + tag_refs+=("refs/tags/$tag") + done < <(jq -r '.tags[].name' "$HANDOFF_DIR/release-handoff.json") + git -C "$source" push --atomic origin "$RELEASE_COMMIT:$EXPECTED_REF" "${tag_refs[@]}" + + - name: Publish verified delegated tarballs via npm trusted publishing + uses: InditexTech/gh-actions/npm@977030536dbdb52a7fe2fd5979510fd6a225d035 # v1.1.0 + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + with: + working-directory: ${{ vars.WORKING_DIRECTORY }} + project-type: ${{ vars.PROJECT_TYPE }} + dist-tag: latest + artifact-directory: ${{ runner.temp }}/publish-dist + + prepare-delegated-next-development: + name: Prepare delegated next-development source + needs: [release-core, publish-npm-delegated] + if: >- + vars.RELEASE_LIFECYCLE == 'delegated' && + needs.release-core.result == 'success' && + needs.publish-npm-delegated.result == 'success' + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + outputs: + handoff_digest: ${{ steps.next-development.outputs.handoff_digest }} + steps: + - name: Download immutable delegated release handoff + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: node-delegated-release-handoff + path: ${{ runner.temp }}/node-delegated-release-handoff + + - name: Restore verified release source + id: source + env: + HANDOFF_DIGEST: ${{ needs.release-core.outputs.handoff_digest }} + HANDOFF_DIR: ${{ runner.temp }}/node-delegated-release-handoff + shell: bash + run: | + set -euo pipefail + metadata="$HANDOFF_DIR/release-handoff.json" + source_bundle="$HANDOFF_DIR/release-source.bundle" + if [[ ! -f "$metadata" || ! -f "$source_bundle" ]] \ + || [[ "$(sha256sum "$metadata" | awk '{print $1}')" != "$HANDOFF_DIGEST" ]] \ + || [[ "$(sha256sum "$source_bundle" | awk '{print $1}')" != "$(jq -r '.source_bundle.sha256' "$metadata")" ]]; then + echo "::error title=Tampered release handoff::Next-development preparation requires the original verified release handoff." + exit 1 + fi + jq -e \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ + '.schema_version == 1 and .repository == $repository and .run_id == $run_id and .run_attempt == $run_attempt and (.release_commit | test("^[0-9a-f]{40}$"))' \ + "$metadata" > /dev/null + rm -rf "$HANDOFF_DIR/source" + git clone --quiet --no-checkout "$source_bundle" "$HANDOFF_DIR/source" + release_commit="$(jq -r '.release_commit' "$metadata")" + git -C "$HANDOFF_DIR/source" checkout --quiet --detach "$release_commit" + echo "release_commit=$release_commit" >> "$GITHUB_OUTPUT" + echo "source_root=$HANDOFF_DIR/source" >> "$GITHUB_OUTPUT" - name: Read governed tool versions id: tool-versions - working-directory: ${{ env.WORKING_DIRECTORY }} + working-directory: ${{ steps.source.outputs.source_root }}/${{ env.WORKING_DIRECTORY }} shell: bash run: | set -euo pipefail @@ -371,38 +621,84 @@ jobs: tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} asdf_version: ${{ env.ASDF_BRANCH_VERSION }} - - name: Install dependencies - working-directory: ${{ env.WORKING_DIRECTORY }} + - name: Prepare next development source + env: + RELEASES: ${{ needs.release-core.outputs.releases }} + SOURCE_ROOT: ${{ steps.source.outputs.source_root }} + working-directory: ${{ steps.source.outputs.source_root }}/${{ env.WORKING_DIRECTORY }} shell: bash run: | set -euo pipefail + git -C "$SOURCE_ROOT" config user.name "github-actions[bot]" + git -C "$SOURCE_ROOT" config user.email "41898282+github-actions[bot]@users.noreply.github.com" + release_bump="$(jq -er '.[0].release_bump | select(type == "string" and length > 0)' <<< "$RELEASES")" case "${PACKAGE_MANAGER:-npm}" in - npm) npm ci --ignore-scripts ;; - pnpm) corepack enable; pnpm install --frozen-lockfile --ignore-scripts ;; + npm) RELEASE_BUMP="$release_bump" npm run version:development ;; + pnpm) corepack enable; RELEASE_BUMP="$release_bump" pnpm run version:development ;; *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; esac + git -C "$SOURCE_ROOT" add --update -- "$WORKING_DIRECTORY" + if git -C "$SOURCE_ROOT" diff --cached --quiet; then + echo "::error title=Missing next-development update::version:development did not produce a source change." + exit 1 + fi + git -C "$SOURCE_ROOT" commit -m "[node-release] prepare for next development iteration" - - name: Run project release:perform - working-directory: ${{ env.WORKING_DIRECTORY }} + - name: Create next-development handoff + id: next-development env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + HANDOFF_DIGEST: ${{ needs.release-core.outputs.handoff_digest }} + HANDOFF_DIR: ${{ runner.temp }}/node-delegated-release-handoff + RELEASE_COMMIT: ${{ steps.source.outputs.release_commit }} + SOURCE_ROOT: ${{ steps.source.outputs.source_root }} shell: bash run: | set -euo pipefail - case "${PACKAGE_MANAGER:-npm}" in - npm) npm run release:perform ;; - pnpm) pnpm run release:perform ;; - *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; - esac + next_development_commit="$(git -C "$SOURCE_ROOT" rev-parse HEAD)" + if ! git -C "$SOURCE_ROOT" merge-base --is-ancestor "$RELEASE_COMMIT" "$next_development_commit"; then + echo "::error title=Invalid next-development provenance::The next-development commit is not descended from the published release." + exit 1 + fi + git -C "$SOURCE_ROOT" bundle create "$HANDOFF_DIR/next-development.bundle" "$next_development_commit" + bundle_sha256="$(sha256sum "$HANDOFF_DIR/next-development.bundle" | awk '{print $1}')" + jq -n \ + --arg bundle_sha256 "$bundle_sha256" \ + --arg handoff_digest "$HANDOFF_DIGEST" \ + --arg next_development_commit "$next_development_commit" \ + --arg release_commit "$RELEASE_COMMIT" \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ + --arg run_id "$GITHUB_RUN_ID" \ + '{ + schema_version: 1, + repository: $repository, + run_id: $run_id, + run_attempt: $run_attempt, + release_handoff_digest: $handoff_digest, + release_commit: $release_commit, + next_development_commit: $next_development_commit, + source_bundle: {file: "next-development.bundle", sha256: $bundle_sha256} + }' > "$HANDOFF_DIR/next-development-handoff.json" + echo "handoff_digest=$(sha256sum "$HANDOFF_DIR/next-development-handoff.json" | awk '{print $1}')" >> "$GITHUB_OUTPUT" + + - name: Upload next-development handoff + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: node-delegated-next-development-handoff + path: | + ${{ runner.temp }}/node-delegated-release-handoff/next-development.bundle + ${{ runner.temp }}/node-delegated-release-handoff/next-development-handoff.json + if-no-files-found: error + retention-days: 14 finalize-delegated-release: name: Finalize delegated release (next dev + sync PR) - needs: [release-core, publish-npm-delegated] + needs: [release-core, publish-npm-delegated, prepare-delegated-next-development] if: >- vars.RELEASE_LIFECYCLE == 'delegated' && needs.release-core.result == 'success' && - needs.publish-npm-delegated.result == 'success' + needs.publish-npm-delegated.result == 'success' && + needs.prepare-delegated-next-development.result == 'success' runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: @@ -425,60 +721,73 @@ jobs: ref: ${{ github.event.pull_request.base.ref || github.ref_name }} token: ${{ steps.app-token.outputs.token }} - - name: Read governed tool versions - id: tool-versions - working-directory: ${{ env.WORKING_DIRECTORY }} - shell: bash - run: | - set -euo pipefail - if grep -Evq '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$' .tool-versions; then - echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." - exit 1 - fi - { - echo "tool_versions<> "$GITHUB_OUTPUT" - - - name: Set up asdf-managed Node - uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1 + - name: Download immutable next-development handoff + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} - asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + name: node-delegated-next-development-handoff + path: ${{ runner.temp }}/node-delegated-next-development-handoff - - name: Next development iteration and sync PR + - name: Verify and promote next development source env: APP_SLUG: ${{ steps.app-token.outputs.app-slug }} BASELINE_BRANCH: ${{ github.event.pull_request.base.ref || github.ref_name }} DEVELOPMENT_FLOW: ${{ vars.DEVELOPMENT_FLOW }} + EXPECTED_HANDOFF_DIGEST: ${{ needs.prepare-delegated-next-development.outputs.handoff_digest }} GH_TOKEN: ${{ steps.app-token.outputs.token }} + HANDOFF_DIR: ${{ runner.temp }}/node-delegated-next-development-handoff + RELEASE_HANDOFF_DIGEST: ${{ needs.release-core.outputs.handoff_digest }} RELEASES: ${{ needs.release-core.outputs.releases }} RELEASE_COMMIT: ${{ needs.release-core.outputs.release_commit }} - working-directory: ${{ env.WORKING_DIRECTORY }} shell: bash run: | set -euo pipefail - gh auth setup-git - git config user.name "${APP_SLUG}[bot]" - git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + metadata="$HANDOFF_DIR/next-development-handoff.json" + source_bundle="$HANDOFF_DIR/next-development.bundle" + if [[ ! -f "$metadata" || ! -f "$source_bundle" ]] \ + || [[ "$(sha256sum "$metadata" | awk '{print $1}')" != "$EXPECTED_HANDOFF_DIGEST" ]]; then + echo "::error title=Tampered next-development handoff::The run-bound next-development metadata is missing or changed." + exit 1 + fi + jq -e \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ + --arg release_commit "$RELEASE_COMMIT" \ + --arg release_handoff_digest "$RELEASE_HANDOFF_DIGEST" \ + ' + .schema_version == 1 and + .repository == $repository and + .run_id == $run_id and + .run_attempt == $run_attempt and + .release_commit == $release_commit and + .release_handoff_digest == $release_handoff_digest and + (.next_development_commit | test("^[0-9a-f]{40}$")) and + (.source_bundle.file == "next-development.bundle") and + (.source_bundle.sha256 | test("^[0-9a-f]{64}$")) + ' "$metadata" > /dev/null + if [[ "$(sha256sum "$source_bundle" | awk '{print $1}')" != "$(jq -r '.source_bundle.sha256' "$metadata")" ]]; then + echo "::error title=Tampered next-development handoff::The next-development source bundle digest does not match metadata." + exit 1 + fi - if [[ "$(git rev-parse HEAD)" != "$RELEASE_COMMIT" ]]; then - echo "::notice title=Next-dev skipped::Branch already advanced past the release commit." - else - release_bump="$(jq -r '.[0].release_bump // ""' <<< "$RELEASES")" - case "${PACKAGE_MANAGER:-npm}" in - npm) RELEASE_BUMP="$release_bump" npm run version:development ;; - pnpm) corepack enable; RELEASE_BUMP="$release_bump" pnpm run version:development ;; - *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; - esac - git -C "$GITHUB_WORKSPACE" add --update -- "$WORKING_DIRECTORY" - if ! git diff --cached --quiet; then - git commit -m "[node-release] prepare for next development iteration" - fi - git push origin "HEAD:refs/heads/$BASELINE_BRANCH" + rm -rf "$HANDOFF_DIR/source" + git clone --quiet --no-checkout "$source_bundle" "$HANDOFF_DIR/source" + source="$HANDOFF_DIR/source" + next_development_commit="$(jq -r '.next_development_commit' "$metadata")" + git -C "$source" rev-parse --verify --quiet "${next_development_commit}^{commit}" > /dev/null + if ! git -C "$source" merge-base --is-ancestor "$RELEASE_COMMIT" "$next_development_commit"; then + echo "::error title=Invalid next-development provenance::The next-development source is not descended from the published release." + exit 1 + fi + + gh auth setup-git + git -C "$source" remote set-url origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" + expected_ref="refs/heads/$BASELINE_BRANCH" + if [[ "$(git ls-remote origin "$expected_ref" | cut -f1)" != "$RELEASE_COMMIT" ]]; then + echo "::error title=Release baseline moved::The protected release branch no longer points to the published release." + exit 1 fi + git -C "$source" push origin "$next_development_commit:$expected_ref" if [[ "$DEVELOPMENT_FLOW" != "git-flow" ]]; then echo "::notice title=Sync skipped::Sync-to-develop applies only to git-flow." @@ -492,6 +801,7 @@ jobs: exit 0 fi sync_branch="automated/sync-release-${version}-to-${develop_branch}" + git fetch --no-tags origin "$BASELINE_BRANCH" git switch --force-create "$sync_branch" "origin/$BASELINE_BRANCH" git push --force-with-lease --set-upstream origin "$sync_branch" diff --git a/.github/workflows/code-npm_node-release-core.yml b/.github/workflows/code-npm_node-release-core.yml index b27a7eb..8266cf6 100644 --- a/.github/workflows/code-npm_node-release-core.yml +++ b/.github/workflows/code-npm_node-release-core.yml @@ -13,11 +13,14 @@ on: default: '' outputs: release_commit: - description: Immutable commit used to build the release artifacts. + description: Immutable commit represented by the release handoff. value: ${{ jobs.prepare-release.outputs.release_commit }} releases: description: JSON array of release tags, versions, package lists, and next-dev versions. value: ${{ jobs.prepare-release.outputs.releases }} + handoff_digest: + description: SHA-256 of the run-bound delegated release metadata, or empty outside delegated lifecycle. + value: ${{ jobs.prepare-release.outputs.handoff_digest }} secrets: APP_PRIVATE_KEY: required: true @@ -171,8 +174,9 @@ jobs: outputs: release_commit: ${{ steps.release-metadata.outputs.release_commit }} releases: ${{ steps.release-metadata.outputs.releases }} - expected_ref: ${{ steps.release-plan.outputs.expected_ref }} + expected_ref: ${{ steps.release-plan.outputs.expected_ref || steps.delegated-plan.outputs.expected_ref }} staging_ref: ${{ steps.release-metadata.outputs.staging_ref }} + handoff_digest: ${{ steps.delegated-handoff.outputs.handoff_digest }} steps: - name: Resolve release context id: release-context @@ -222,6 +226,7 @@ jobs: - name: Create GitHub App token id: app-token + if: vars.RELEASE_LIFECYCLE != 'delegated' uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: client-id: ${{ vars.APP_CLIENT_ID }} @@ -235,7 +240,6 @@ jobs: fetch-depth: 0 persist-credentials: false ref: ${{ steps.release-context.outputs.release_ref }} - token: ${{ steps.app-token.outputs.token }} - name: Read governed tool versions id: tool-versions @@ -267,8 +271,8 @@ jobs: run: | set -euo pipefail case "${PACKAGE_MANAGER:-npm}" in - npm) npm ci --ignore-scripts ;; - pnpm) corepack enable; pnpm install --frozen-lockfile --ignore-scripts ;; + npm) npm ci ;; + pnpm) corepack enable; pnpm install --frozen-lockfile ;; *) echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm."; exit 1 ;; esac @@ -582,19 +586,17 @@ jobs: id: delegated-plan if: vars.RELEASE_LIFECYCLE == 'delegated' env: - APP_SLUG: ${{ steps.app-token.outputs.app-slug }} BASELINE_BRANCH: ${{ steps.release-context.outputs.baseline_branch }} DEVELOPMENT_FLOW: ${{ vars.DEVELOPMENT_FLOW }} - GH_TOKEN: ${{ steps.app-token.outputs.token }} RELEASE_LABELS: ${{ steps.release-context.outputs.release_labels }} + RELEASE_SOURCE_COMMIT: ${{ steps.release-context.outputs.release_ref }} working-directory: ${{ env.WORKING_DIRECTORY }} shell: bash run: | set -euo pipefail - gh auth setup-git - git config user.name "${APP_SLUG}[bot]" - git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" case ",$RELEASE_LABELS," in *",release-type/major,"*) release_bump="major" ;; @@ -622,7 +624,10 @@ jobs: esac git tag | sort > "$RUNNER_TEMP/tags-after.txt" - mapfile -t new_tags < <(comm -13 "$RUNNER_TEMP/tags-before.txt" "$RUNNER_TEMP/tags-after.txt") + new_tags=() + while IFS= read -r tag; do + new_tags+=("$tag") + done < <(comm -13 "$RUNNER_TEMP/tags-before.txt" "$RUNNER_TEMP/tags-after.txt") if [[ ${#new_tags[@]} -eq 0 ]]; then echo "::error title=No release cut::release:prepare produced no new tags." exit 1 @@ -631,6 +636,7 @@ jobs: records_file="$RUNNER_TEMP/node-release-records.jsonl" : > "$records_file" for tag in "${new_tags[@]}"; do + tag_target="$(git rev-parse "${tag}^{commit}")" if [[ "$tag" =~ ([0-9]+\.[0-9]+\.[0-9]+([-+.][0-9A-Za-z.-]+)?)$ ]]; then version="${BASH_REMATCH[1]}" else @@ -638,9 +644,10 @@ jobs: fi jq -cn \ --arg tag "$tag" \ + --arg tag_target "$tag_target" \ --arg version "$version" \ --arg bump "$release_bump" \ - '{packages: [], version: $version, tag: $tag, next_dev: "", release_bump: $bump}' \ + '{packages: [], version: $version, tag: $tag, tag_target: $tag_target, next_dev: "", release_bump: $bump}' \ >> "$records_file" done releases="$(jq -cs . "$records_file")" @@ -657,6 +664,7 @@ jobs: { echo "expected_ref=refs/heads/$BASELINE_BRANCH" + echo "source_commit=$RELEASE_SOURCE_COMMIT" echo "primary_version=$primary_version" echo "changelog_pending=$changelog_pending" echo "releases_intermediate<> "$GITHUB_OUTPUT" + + - name: Build and pack delegated distributions + id: delegated-pack + if: vars.RELEASE_LIFECYCLE == 'delegated' + env: + HANDOFF_DIR: ${{ runner.temp }}/node-delegated-release-handoff + RELEASES: ${{ steps.delegated-plan.outputs.releases_intermediate }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + DIST_DIR="$HANDOFF_DIR/tarballs" + RECORDS_FILE="$HANDOFF_DIR/tarballs.jsonl" + rm -rf "$HANDOFF_DIR" + mkdir -p "$DIST_DIR" + : > "$RECORDS_FILE" + + inspect_declared_files() { + local package_dir="$1" + local tarball="$2" + local manifest="$package_dir/package.json" + local declared matched path relative + jq -e ' + .files + | type == "array" and length > 0 and + all(.[]; type == "string" and length > 0 and test("^[^\\r\\n]+$")) + ' "$manifest" > /dev/null + while IFS= read -r declared; do + if [[ "$declared" == /* || "$declared" == "." || "$declared" == ".." || "$declared" == ../* || "$declared" == */../* || "$declared" == */.. || "$declared" == "!"* || "$declared" == *"{"* || "$declared" == *"}"* || "$declared" == *\\* || "$declared" == *"@("* || "$declared" == *"+("* || "$declared" == *"?("* || "$declared" == *"*("* || "$declared" == *"!("* ]]; then + echo "::error title=Unsupported package contents declaration::${manifest} declares an unsafe or unverifiable pattern '$declared'." + exit 1 + fi + matched=0 + if [[ "$declared" == *"*"* || "$declared" == *"?"* || "$declared" == *"["* || "$declared" == *"]"* ]]; then + while IFS= read -r -d '' path; do + relative="${path#"$package_dir"/}" + matched=$((matched + 1)) + if ! tar -tzf "$tarball" | grep -qxF "package/$relative"; then + echo "::error title=Incomplete tarball::$(basename "$tarball") omits declared file '$relative'." + exit 1 + fi + done < <(node - "$package_dir" "$declared" <<'NODE' + const fs = require("fs"); + const path = require("path"); + const root = process.argv[2]; + const pattern = process.argv[3]; + let expression = "^"; + for (let index = 0; index < pattern.length; index += 1) { + const character = pattern[index]; + if (character === "*") { + let stars = 1; + while (pattern[index + stars] === "*") stars += 1; + if (stars > 1 && pattern[index + stars] === "/") { + expression += "(?:[^/]+/)*"; + index += stars; + } else { + expression += stars > 1 ? ".*" : "[^/]*"; + index += stars - 1; + } + } else if (character === "?") { + expression += "[^/]"; + } else if (character === "[") { + const closing = pattern.indexOf("]", index + 1); + if (closing === -1 || closing === index + 1) process.exit(2); + const characterClass = pattern.slice(index + 1, closing); + if (characterClass.includes("/") || characterClass.includes("\\")) process.exit(2); + expression += `[${characterClass[0] === "!" ? "^" + characterClass.slice(1) : characterClass}]`; + index = closing; + } else if (character === "]") { + process.exit(2); + } else { + expression += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&"); + } + } + const matcher = new RegExp(`${expression}$`); + const files = []; + const walk = (directory) => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const candidate = path.join(directory, entry.name); + if (entry.isDirectory()) walk(candidate); + else if (entry.isFile()) files.push(path.relative(root, candidate).split(path.sep).join("/")); + } + }; + walk(root); + for (const file of files.filter((file) => matcher.test(file)).sort()) process.stdout.write(`${path.join(root, file)}\0`); + NODE + ) + elif [[ -d "$package_dir/$declared" ]]; then + while IFS= read -r -d '' path; do + relative="${path#"$package_dir"/}" + matched=$((matched + 1)) + if ! tar -tzf "$tarball" | grep -qxF "package/$relative"; then + echo "::error title=Incomplete tarball::$(basename "$tarball") omits declared file '$relative'." + exit 1 + fi + done < <(find "$package_dir/$declared" -type f -print0) + elif [[ -f "$package_dir/$declared" ]]; then + matched=1 + if ! tar -tzf "$tarball" | grep -qxF "package/$declared"; then + echo "::error title=Incomplete tarball::$(basename "$tarball") omits declared file '$declared'." + exit 1 + fi + fi + if [[ "$matched" -eq 0 ]]; then + echo "::error title=Missing declared package content::${manifest} declaration '$declared' matches no built files." + exit 1 + fi + done < <(jq -r '.files[]' "$manifest") + } + + release_tag_for() { + local package_name="$1" + local package_version="$2" + local member record release_tag release_version + local -a exact_matches=() + local -a version_matches=() + member="$(printf '%s' "${package_name##*/}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" + while IFS= read -r record; do + release_tag="$(jq -r '.tag' <<< "$record")" + release_version="$(jq -r '.version' <<< "$record")" + [[ "$release_version" == "$package_version" ]] || continue + version_matches+=("$release_tag") + if [[ "$release_tag" == "$package_version" || "$release_tag" == "${member}-${package_version}" || "$release_tag" == "${package_name}-${package_version}" ]]; then + exact_matches+=("$release_tag") + fi + done < <(jq -c '.[]' <<< "$RELEASES") + if [[ ${#exact_matches[@]} -eq 1 ]]; then + printf '%s' "${exact_matches[0]}" + elif [[ ${#exact_matches[@]} -eq 0 && ${#version_matches[@]} -eq 1 ]]; then + printf '%s' "${version_matches[0]}" + else + echo "::error title=Ambiguous package release::Package '$package_name@$package_version' does not resolve to exactly one prepared release tag." >&2 + exit 1 + fi + } + + pack_package() { + local package_dir="$1" + local package_name="$2" + local before="$HANDOFF_DIR/tarballs-before.txt" + local after="$HANDOFF_DIR/tarballs-after.txt" + local added="$HANDOFF_DIR/tarballs-added.txt" + local tarball + find "$DIST_DIR" -maxdepth 1 -name '*.tgz' -print | LC_ALL=C sort > "$before" + case "${PACKAGE_MANAGER:-npm}" in + npm) + npm pack "./$package_dir" --pack-destination "$DIST_DIR" + ;; + pnpm) + corepack enable + ( cd "$package_dir" && pnpm pack --pack-destination "$DIST_DIR" ) + ;; + *) + echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm." + exit 1 + ;; + esac + find "$DIST_DIR" -maxdepth 1 -name '*.tgz' -print | LC_ALL=C sort > "$after" + comm -13 "$before" "$after" > "$added" + if [[ "$(wc -l < "$added" | tr -d ' ')" -ne 1 ]]; then + echo "::error title=Invalid package artifact::Packing '$package_name' must produce exactly one new tarball." + exit 1 + fi + tarball="$(<"$added")" + inspect_declared_files "$package_dir" "$tarball" + tarball_package="$(tar -xzOf "$tarball" package/package.json | jq -er '.name | select(type == "string" and length > 0)')" + tarball_version="$(tar -xzOf "$tarball" package/package.json | jq -er '.version | select(type == "string" and length > 0)')" + if [[ "$tarball_package" != "$package_name" ]]; then + echo "::error title=Invalid package artifact::$(basename "$tarball") identifies '$tarball_package', not declared package '$package_name'." + exit 1 + fi + tarball_release_tag="$(release_tag_for "$tarball_package" "$tarball_version")" + jq -cn \ + --arg file "$(basename "$tarball")" \ + --arg package "$package_name" \ + --arg release_tag "$tarball_release_tag" \ + --arg sha256 "$(sha256sum "$tarball" | awk '{print $1}')" \ + --arg version "$tarball_version" \ + '{file: $file, package: $package, release_tag: $release_tag, sha256: $sha256, version: $version}' >> "$RECORDS_FILE" + } + + case "${PACKAGE_MANAGER:-npm}" in + npm) + npm run build + ;; + pnpm) + corepack enable + pnpm run build + ;; + *) + echo "::error title=Invalid package manager::PACKAGE_MANAGER must be npm or pnpm." + exit 1 + ;; + esac + + case "$PROJECT_TYPE" in + single) + package_name="$(jq -er '.name | select(type == "string" and length > 0)' package.json)" + pack_package "." "$package_name" + ;; + workspaces) + jq -e ' + .release.packages + | type == "array" and length > 0 and + all(.[]; type == "string" and length > 0) + ' "$GITHUB_WORKSPACE/.github/inditextech-ci-node.json" > /dev/null + while IFS= read -r package_name; do + member="$(printf '%s' "${package_name##*/}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//')" + package_dir="packages/$member" + if [[ ! -f "$package_dir/package.json" ]] || [[ "$(jq -r '.name' "$package_dir/package.json")" != "$package_name" ]]; then + echo "::error title=Unknown workspace package::release.packages entry '$package_name' does not resolve to its declared workspace manifest." + exit 1 + fi + pack_package "$package_dir" "$package_name" + done < <(jq -r '.release.packages[]' "$GITHUB_WORKSPACE/.github/inditextech-ci-node.json") + ;; + *) + echo "::error title=Invalid project type::PROJECT_TYPE must be single or workspaces." + exit 1 + ;; + esac + + jq -cs . "$RECORDS_FILE" > "$HANDOFF_DIR/tarballs.json" + echo "handoff_dir=$HANDOFF_DIR" >> "$GITHUB_OUTPUT" + + - name: Create delegated release handoff + id: delegated-handoff + if: vars.RELEASE_LIFECYCLE == 'delegated' + env: + EXPECTED_REF: ${{ steps.delegated-plan.outputs.expected_ref }} + HANDOFF_DIR: ${{ steps.delegated-pack.outputs.handoff_dir }} + RELEASES: ${{ steps.delegated-plan.outputs.releases_intermediate }} + RELEASE_COMMIT: ${{ steps.delegated-source.outputs.release_commit }} + SOURCE_COMMIT: ${{ steps.delegated-plan.outputs.source_commit }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + if ! git merge-base --is-ancestor "$SOURCE_COMMIT" "$RELEASE_COMMIT"; then + echo "::error title=Invalid release provenance::The prepared release is not descended from the verified source commit." + exit 1 + fi + tag_refs=() - while IFS= read -r tag; do + while IFS= read -r record; do + tag="$(jq -r '.tag' <<< "$record")" + tag_target="$(jq -r '.tag_target' <<< "$record")" git check-ref-format --allow-onelevel "refs/tags/$tag" + if [[ "$(git rev-parse "${tag}^{commit}")" != "$tag_target" ]]; then + echo "::error title=Invalid release tag::Prepared tag '$tag' no longer points at its recorded commit." + exit 1 + fi + if ! git merge-base --is-ancestor "$SOURCE_COMMIT" "$tag_target" \ + || ! git merge-base --is-ancestor "$tag_target" "$RELEASE_COMMIT"; then + echo "::error title=Invalid release tag provenance::Prepared tag '$tag' must satisfy source <= tag <= release." + exit 1 + fi tag_refs+=("refs/tags/$tag") - done < <(jq -r '.[].tag' <<< "$RELEASES") - git push --atomic origin "HEAD:$EXPECTED_REF" "${tag_refs[@]}" + done < <(jq -c '.[]' <<< "$RELEASES") + + while IFS= read -r tarball; do + release_tag="$(jq -r '.release_tag' <<< "$tarball")" + package_name="$(jq -r '.package' <<< "$tarball")" + package_version="$(jq -r '.version' <<< "$tarball")" + if [[ "$(jq -r --arg tag "$release_tag" --arg version "$package_version" '[.[] | select(.tag == $tag and .version == $version)] | length' <<< "$RELEASES")" -ne 1 ]]; then + echo "::error title=Invalid package release::Tarball '$package_name@$package_version' is not bound to exactly one prepared release tag." + exit 1 + fi + done < <(jq -c '.[]' "$HANDOFF_DIR/tarballs.json") + + git bundle create "$HANDOFF_DIR/release-source.bundle" "$RELEASE_COMMIT" "${tag_refs[@]}" + source_bundle_sha256="$(sha256sum "$HANDOFF_DIR/release-source.bundle" | awk '{print $1}')" + jq -n \ + --arg expected_ref "$EXPECTED_REF" \ + --arg release_commit "$RELEASE_COMMIT" \ + --arg repository "$GITHUB_REPOSITORY" \ + --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg source_bundle_sha256 "$source_bundle_sha256" \ + --arg source_commit "$SOURCE_COMMIT" \ + --argjson releases "$RELEASES" \ + --slurpfile tarballs "$HANDOFF_DIR/tarballs.json" \ + '{ + schema_version: 1, + repository: $repository, + run_id: $run_id, + run_attempt: $run_attempt, + expected_ref: $expected_ref, + source_commit: $source_commit, + release_commit: $release_commit, + releases: $releases, + tags: [$releases[] | {name: .tag, target: .tag_target}], + source_bundle: {file: "release-source.bundle", sha256: $source_bundle_sha256}, + tarballs: $tarballs[0] + }' > "$HANDOFF_DIR/release-handoff.json" + handoff_digest="$(sha256sum "$HANDOFF_DIR/release-handoff.json" | awk '{print $1}')" + rm -f \ + "$HANDOFF_DIR/tarballs.json" \ + "$HANDOFF_DIR/tarballs.jsonl" \ + "$HANDOFF_DIR/tarballs-added.txt" \ + "$HANDOFF_DIR/tarballs-after.txt" \ + "$HANDOFF_DIR/tarballs-before.txt" + echo "handoff_digest=$handoff_digest" >> "$GITHUB_OUTPUT" + + - name: Upload delegated release handoff + if: vars.RELEASE_LIFECYCLE == 'delegated' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: node-delegated-release-handoff + path: ${{ steps.delegated-pack.outputs.handoff_dir }} + if-no-files-found: error + retention-days: 14 - name: Publish release metadata id: release-metadata @@ -766,7 +1059,11 @@ jobs: staging_ref="" fi { - echo "release_commit=$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" + if [[ "$RELEASE_LIFECYCLE" == "delegated" ]]; then + echo "release_commit=${{ steps.delegated-source.outputs.release_commit }}" + else + echo "release_commit=$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" + fi echo "staging_ref=$staging_ref" echo "releases< Date: Fri, 11 Sep 2026 01:29:06 +0000 Subject: [PATCH 32/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:ab5c552c601fb6e47fa43d3b07fc8eb4566c0e6172fcd6f76271c185bf9e65d2 From 298ae3da3d2a4e7502a32faf7b2d6d27585cb29b Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:48:57 +0000 Subject: [PATCH 33/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a5f1884185f918e73c6f8f9c2c8c58e8b08b53a73f8333c67f0ff101ec5e6a07 From 91612ec8630e7545344ae5947ee5272507799cbd Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:34:19 +0000 Subject: [PATCH 34/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:43d2bd9aca68b3ab75d8836fc11c63db800fe4a7e9e1944a3c91fd5620584cf8 From dc1fde85e456834c252c4d1e39446f14368367fa Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 03:28:39 +0000 Subject: [PATCH 35/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:92d5136cdd5e7369a70f0d467a5e1ad55eaaab1961d832037a603853a3d5499b From 4cb7176c9c269f5014ed99cf93a2fafb447f91a2 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:32:37 +0000 Subject: [PATCH 36/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:62c9ca25565e46a433ec3576ee8277d2093803fc0013d61563aeba2bfd265d4e From f35ff2bc663efde5bb433f5ecde8d0fa5f3c9166 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:23:24 +0000 Subject: [PATCH 37/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:5c754083c5edd82d09fbc963e4382cc72582a4470994705efcd9a9588df659b7 --- .github/inditextech-ci-sync-manifest.json | 12 ++++++------ .github/workflows/pr-verify.yml | 3 +-- .github/workflows/push-verify.yml | 3 +-- .github/workflows/scorecard-analysis.yml | 21 +++++++++++++++++++++ .github/workflows/sync-to-develop.yml | 4 +++- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json index 765382e..7d9e2d9 100644 --- a/.github/inditextech-ci-sync-manifest.json +++ b/.github/inditextech-ci-sync-manifest.json @@ -13,7 +13,7 @@ "creation_year": 2026, "integrity": { "algorithm": "hmac-sha256", - "signature": "6cbdc2ef01327a5d5d9bc3da231434bb01b83dc5878f319398798c12c3904c5e" + "signature": "3633e607144a5e6a891f585605a8730dc743cbdf59c2c5f67164d11e77232b10" }, "managed_by": "InditexTech CI governance", "managed_paths": { @@ -29,10 +29,10 @@ ".github/workflows/code-npm_node-sonarcloud-analysis.yml": "70c1c21825cde574d4744b308e198d030dbabddaa0f8ba07980be253ea6d7afa", ".github/workflows/code-release_preview.yml": "4e2e95d60a498eb12d97ba5c8330b1173f04b02c807140beddad06a6e225b166", ".github/workflows/codeql.yml": "633c0f288566fd1408e7ad545c7631e05f861ebf4ebdce3fe9586672ef065d97", - ".github/workflows/pr-verify.yml": "f52295fbfe81578c0a6459d8b384be569933ac78d32fff370932a8dec6943e46", - ".github/workflows/push-verify.yml": "fd700ba0ee23fdee9cd7ecf23ad386247f4b621b73cc1946f9ef0eda4ea07086", - ".github/workflows/scorecard-analysis.yml": "5bd6da647f708cced5d33411cc27aac7f7ee2be06ae541defea677d70fe80481", - ".github/workflows/sync-to-develop.yml": "5c35cf0b946a939d0324b1530a78e319e979f993bd1f0105e64fc4478395d8d7", + ".github/workflows/pr-verify.yml": "b39240fd362fb004c3b47cd4189cb527d9e27a7c37645bbe6324d2a757e34f7f", + ".github/workflows/push-verify.yml": "49e6e9c6a7015ea59a2bc831982b2fc4bfd1d5721302f6041170b4a15b7d56e1", + ".github/workflows/scorecard-analysis.yml": "9866047c5d638ee5d9246e646400cad473ce8ab0f66b444337405b8f9f324f6e", + ".github/workflows/sync-to-develop.yml": "1b2d0f33094052f771d91a7e7c29a666e8a9cee5cc5ca28e1f40168920ca2702", ".tool-versions": "5e7b05edf5d8df174df5dd99d012e57666ec9923ac8506df7dc7753ba41815fd", "CODE_OF_CONDUCT.md": "ce1e7a8f68a7917d48c03f9f7aae5529367f73af0e959276e889d33ea1e8d4ab", "CONTRIBUTING.md": "4e1264ca54a45df44b362c7533f0eba912bfa77b6f561121ae9e5e9d6aa00df3", @@ -50,7 +50,7 @@ "retained_paths": {}, "schema_version": 2, "source_digests": { - "base": "5070fc83e3b1e67ddc4c2463e767ff3d0d364115c6433d2ae0de75afef2e0e63", + "base": "84ae844059a60d9ea8bc96910d88c912b9875aa90e48411b243f119f584fd571", "node": "d2f8082132a512a1770518d0995acf54caa0690a8e0fea20b5691805c6a5473c" } } diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml index 66c196d..8d73d82 100644 --- a/.github/workflows/pr-verify.yml +++ b/.github/workflows/pr-verify.yml @@ -19,7 +19,6 @@ jobs: timeout-minutes: 10 permissions: contents: read - checks: write # Lets reviewdog publish actionlint check results. steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -31,7 +30,7 @@ jobs: - name: Lint GitHub Actions workflows uses: reviewdog/action-actionlint@dbe5299849118fd6f099ba563d263d770955a64a # v1.73.2 with: - reporter: github-pr-check + reporter: github-annotations fail_level: error repo-linter: diff --git a/.github/workflows/push-verify.yml b/.github/workflows/push-verify.yml index b5c40f9..fc23062 100644 --- a/.github/workflows/push-verify.yml +++ b/.github/workflows/push-verify.yml @@ -22,7 +22,6 @@ jobs: timeout-minutes: 10 permissions: contents: read - checks: write # Lets reviewdog publish actionlint check results. steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -32,7 +31,7 @@ jobs: - name: Lint GitHub Actions workflows uses: reviewdog/action-actionlint@dbe5299849118fd6f099ba563d263d770955a64a # v1.73.2 with: - reporter: github-check + reporter: github-annotations fail_level: error repo-linter: diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index 7ee0523..36f5f92 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -49,8 +49,29 @@ jobs: path: results.sarif retention-days: 5 + - name: Resolve code scanning availability + id: scanning-availability + env: + GH_TOKEN: ${{ github.token }} + FORCE_ENABLED: ${{ vars.CODE_SCANNING_ENABLED }} + run: | + set -euo pipefail + visibility="$(gh api "/repos/${GITHUB_REPOSITORY}" --jq '.visibility' 2>/dev/null || echo unknown)" + seat="$(gh api "/repos/${GITHUB_REPOSITORY}" --jq '.security_and_analysis.advanced_security.status // "unknown"' 2>/dev/null || echo unknown)" + enabled=false + if [ "${visibility}" = "public" ] || [ "${seat}" = "enabled" ] || [ "${FORCE_ENABLED:-}" = "true" ]; then + enabled=true + fi + echo "Code scanning availability: visibility=${visibility} seat=${seat} enabled=${enabled}" + echo "enabled=${enabled}" >> "${GITHUB_OUTPUT}" + - name: Upload SARIF to Code Scanning + if: steps.scanning-availability.outputs.enabled == 'true' uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 with: sarif_file: results.sarif category: scorecard + + - name: Report unavailable Code Scanning + if: steps.scanning-availability.outputs.enabled != 'true' + run: echo "::notice::Scorecard SARIF is retained as an artifact; Code Scanning ingestion is unavailable." diff --git a/.github/workflows/sync-to-develop.yml b/.github/workflows/sync-to-develop.yml index de7c21a..344251a 100644 --- a/.github/workflows/sync-to-develop.yml +++ b/.github/workflows/sync-to-develop.yml @@ -27,6 +27,8 @@ jobs: concurrency: group: sync-to-develop-${{ github.repository }}-${{ github.event.pull_request.base.ref }} cancel-in-progress: false + env: + SYNC_BASE_REF: ${{ github.event.pull_request.base.ref }} steps: # The checkout runs *before* any App credential exists. This event carries # the base repository's secrets, so ordering is the cheap half of the @@ -40,7 +42,7 @@ jobs: # Deliberately the base branch, not the merge SHA. Central policy # requires this ref, and the branch is trusted here: this event only # fires for an already-merged pull request. - ref: ${{ github.event.pull_request.base.ref }} + ref: ${{ env.SYNC_BASE_REF }} - name: Create GitHub App token id: app-token From ea11ccc1816f6a620d4f0bcae351a2d867a51491 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:25:43 +0000 Subject: [PATCH 38/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:ffbc5eff74f06e105c82ae92ee348f563b763490b552f48886beb4228253d858 From 33a4d9b0de761bfabbcbdd1a1a455d99771627ee Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:58:39 +0000 Subject: [PATCH 39/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:7420a5b2230385f4359ba932c2ca5a5e3696b9b3cda0c55f8d773144ba2cccae From 93efbe102907a5b63d660e724027c4474e5f77c2 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:02:01 +0000 Subject: [PATCH 40/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:1f4fa23a2279db3bb0ef22fc9c2500fdb6e6f4244cc82664857c776e60282fbe From 3ebcf650c84d2dc8e2474aafdc5418f12c5dcc3d Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:14:20 +0000 Subject: [PATCH 41/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:9325f0b942f60a72cbad4ce5ffbcc8cb8c5376291b36bedbd0bd47c8526e5947 From 6f8215a5faf148647118ce3799588344decebb4d Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:57:46 +0000 Subject: [PATCH 42/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:f22ecee185c5daf177b8d3afe375c714bf91b55fd77242561c18d8dd8eb77d43 From 99d209230c0f4d4037c64429324e928ae0c00166 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:19:13 +0000 Subject: [PATCH 43/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a08926e561e8f636f9038453b2df6896ca55523a10750d08e9abbbb6626a5fff --- .github/inditextech-ci-sync-manifest.json | 6 +++--- .../code-npm_node-publish-release-and-snapshot.yml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json index 7d9e2d9..a8600c6 100644 --- a/.github/inditextech-ci-sync-manifest.json +++ b/.github/inditextech-ci-sync-manifest.json @@ -13,7 +13,7 @@ "creation_year": 2026, "integrity": { "algorithm": "hmac-sha256", - "signature": "3633e607144a5e6a891f585605a8730dc743cbdf59c2c5f67164d11e77232b10" + "signature": "97bf3538d9b8ae0b19faa650c65311fc2cbcf3d80c3683d5b3fb6454a9ec43df" }, "managed_by": "InditexTech CI governance", "managed_paths": { @@ -24,7 +24,7 @@ ".github/PULL_REQUEST_TEMPLATE.md": "23a0b0ac79a9020ccac9c013582a01f86e2df2ae7f914673583b9a3368313041", ".github/inditextech-ci-node.json": "72449d1243d714b1ef9bd615e6dc67d0ec0b25f0c73440a08fa34d1e498864ac", ".github/workflows/code-npm_node-PR_verify.yml": "89e11bc8aae9ec44ab47222cc570c27dffb8b426e88665598ea7065a9cc95d54", - ".github/workflows/code-npm_node-publish-release-and-snapshot.yml": "878a28f8b8042411ccd8123a95aca6132c083a2d2da7fd0d10095997c9678764", + ".github/workflows/code-npm_node-publish-release-and-snapshot.yml": "9b9de7bf2f53cfad5c2ab1bc76443844d4aa8958089bf1288f8d35031d55095e", ".github/workflows/code-npm_node-release-core.yml": "3b409b668d51f67bb683b56905d314fd807de32f4f4c46555ce9de4cb3bc3c73", ".github/workflows/code-npm_node-sonarcloud-analysis.yml": "70c1c21825cde574d4744b308e198d030dbabddaa0f8ba07980be253ea6d7afa", ".github/workflows/code-release_preview.yml": "4e2e95d60a498eb12d97ba5c8330b1173f04b02c807140beddad06a6e225b166", @@ -51,6 +51,6 @@ "schema_version": 2, "source_digests": { "base": "84ae844059a60d9ea8bc96910d88c912b9875aa90e48411b243f119f584fd571", - "node": "d2f8082132a512a1770518d0995acf54caa0690a8e0fea20b5691805c6a5473c" + "node": "9715f8dd4f7652754d6bb14dd3ecbbceaf4364ae66934aaa27bbabf0d2ae5a5c" } } diff --git a/.github/workflows/code-npm_node-publish-release-and-snapshot.yml b/.github/workflows/code-npm_node-publish-release-and-snapshot.yml index d39af6f..03946f3 100644 --- a/.github/workflows/code-npm_node-publish-release-and-snapshot.yml +++ b/.github/workflows/code-npm_node-publish-release-and-snapshot.yml @@ -56,6 +56,7 @@ jobs: timeout-minutes: 5 permissions: contents: read + pull-requests: read outputs: head_sha: ${{ steps.resolve.outputs.head_sha }} steps: From 32ee3bab06ab02aca9898b29dd8d42849b7ad184 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:24:41 +0000 Subject: [PATCH 44/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:c0fabc035083fc3defb0f7407ed48fe1234d9239e1e8def5d0c235eeb88d60d2 From 48cbbbbc46a9ffcec3dd79c1dd348c2336fc76ca Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 17:57:44 +0000 Subject: [PATCH 45/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:1d4a348c1b95e70e772e1e434dde17c10981183a3e1bab0a63b5df67e30658be From 7fa3ebe28e58ef8cfe96a4332ffc819fc242b720 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:18:31 +0000 Subject: [PATCH 46/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:74781165a212baf9ed6f4598561a8bf42fb002229c3128bd8f556af3d3df894b From 5d2c89ee6fbdd446f55d788e68b20777ef98893a Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 04:27:41 +0000 Subject: [PATCH 47/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:057d5091f0d32ff848bcf704af7f55ec3c7b5786fa65a21efb73069d8f1985c7 From 0d9e45eb69ad93ddc8bc2021ad008e9c64823ba0 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:20:19 +0000 Subject: [PATCH 48/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:3c1583d36f67a165a5cba2a3a1b53b17a0d3ce8eab779f3609f39ea0ea186984 From 7bebda4ca20f3ed35f93088accd225371bfd55de Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:42:10 +0000 Subject: [PATCH 49/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:3a8e1651bc5d13f2e87ac0c17bfaf623b65aae59dcd6920938ea94999bcddaff From a346a2488ad3705a5724690719e348f80d23cc01 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:18:27 +0000 Subject: [PATCH 50/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a4ef74f590730fc1017d920296766afd7b636abb1755b56fa1a2a74adb3f442b From de6ec1c8a15afb4772a96c5164d873d35ecc923a Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:04:04 +0000 Subject: [PATCH 51/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a9b538ac3cbf0d1a1f562d0e35e412309e18d2fdf596014ea97dd9cfc27d7b7b From 9050b27379ad3de742f439af412213d416aab179 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 07:25:02 +0000 Subject: [PATCH 52/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:13c275fc0c3ad747aac5ec0b2ecab81525794cb7ce5d10e1bb101040e36391fe From 56242e9226f44a6fc53b09d32458b67710c445b2 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:22:31 +0000 Subject: [PATCH 53/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:4a4f1e20ee74786daca482986bd9a42b918e68d1dd4df75ebf8bc758e9b4cc36 From 3688ffd9e8011c9223d11f867f51bf3a5cef7d6b Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:27:39 +0000 Subject: [PATCH 54/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:782052309153e240036cdb34a1a181173f565865b96be7f57dd6964cad8a7296 From 3c86dbef75c29beb8a80ebe41d3ba59bb21bd6f7 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:32:58 +0000 Subject: [PATCH 55/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:5f6f48ad27d87195180d2b8a4bfa158a43b76edf4f9fd4cce6600faca22009a1 From f9c39f8bacb9850f82f4405d770fc22b63c67822 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 01:03:54 +0000 Subject: [PATCH 56/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:fc2af247cc496d2d040295d4839c31fbabca880085283ef0c2101f48a20bac11 From b26222d09edde22ea0b4ffd040639b21e002a8be Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 04:34:17 +0000 Subject: [PATCH 57/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:ad8b6d976f6f209afd5ec595ed60e481fe69b38537f45d4594c62d5b7544045d From 4108d8423994e37b81242d0d2f3b2586fbd48d4d Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:27:53 +0000 Subject: [PATCH 58/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:23cf5b638dc70d36feb8a0213957907053e4d40b683b654860796dcd1dcf3168 --- .github/inditextech-ci-sync-manifest.json | 6 +++--- .github/workflows/codeql.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json index a8600c6..11930fb 100644 --- a/.github/inditextech-ci-sync-manifest.json +++ b/.github/inditextech-ci-sync-manifest.json @@ -13,7 +13,7 @@ "creation_year": 2026, "integrity": { "algorithm": "hmac-sha256", - "signature": "97bf3538d9b8ae0b19faa650c65311fc2cbcf3d80c3683d5b3fb6454a9ec43df" + "signature": "b9d23187f5e79e153c11a968190ef262052db1a09dbfd3df78798ca1bb12d6ff" }, "managed_by": "InditexTech CI governance", "managed_paths": { @@ -28,7 +28,7 @@ ".github/workflows/code-npm_node-release-core.yml": "3b409b668d51f67bb683b56905d314fd807de32f4f4c46555ce9de4cb3bc3c73", ".github/workflows/code-npm_node-sonarcloud-analysis.yml": "70c1c21825cde574d4744b308e198d030dbabddaa0f8ba07980be253ea6d7afa", ".github/workflows/code-release_preview.yml": "4e2e95d60a498eb12d97ba5c8330b1173f04b02c807140beddad06a6e225b166", - ".github/workflows/codeql.yml": "633c0f288566fd1408e7ad545c7631e05f861ebf4ebdce3fe9586672ef065d97", + ".github/workflows/codeql.yml": "fcfd5d629157760bcc5666b5193ed01223ab82e176f19844eaef7016278168be", ".github/workflows/pr-verify.yml": "b39240fd362fb004c3b47cd4189cb527d9e27a7c37645bbe6324d2a757e34f7f", ".github/workflows/push-verify.yml": "49e6e9c6a7015ea59a2bc831982b2fc4bfd1d5721302f6041170b4a15b7d56e1", ".github/workflows/scorecard-analysis.yml": "9866047c5d638ee5d9246e646400cad473ce8ab0f66b444337405b8f9f324f6e", @@ -51,6 +51,6 @@ "schema_version": 2, "source_digests": { "base": "84ae844059a60d9ea8bc96910d88c912b9875aa90e48411b243f119f584fd571", - "node": "9715f8dd4f7652754d6bb14dd3ecbbceaf4364ae66934aaa27bbabf0d2ae5a5c" + "node": "d35cb892d1e48292852cdb39e10a31b4583849fd4aeb71c2e4434c7e37c4e82e" } } diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0ec0eb2..f763268 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -93,7 +93,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4 + uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4 with: build-mode: ${{ matrix.build-mode }} languages: ${{ matrix.language }} @@ -104,6 +104,6 @@ jobs: id: actions/cache-poisoning/poisonable-step - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4 + uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4 with: category: /language:${{ matrix.language }} From 5bb170feaa6684e0881c1bfa57b9f77e5e5aee96 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:33:24 +0000 Subject: [PATCH 59/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:7e79d3961243cac5246505d18905e8d17a43f931da179784ba7624714420cd96 From 2bf5d6a3a85d13149c8d68c586d7c38642b682a6 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:31:04 +0000 Subject: [PATCH 60/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:d6f9ad0e0099e1f7c2d7503848bd13530150354eb0ae7d587fabd22a8ad80402 From b4d72d9c2bea880e874044f4943342982b8c23be Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 17:51:30 +0000 Subject: [PATCH 61/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:fd264976f6918d5054d088eb80d5174fce9d4a0d9ae4dbcf3784ff7261f9c9b5 From 44fe266ed886cd0750f3eda0734c383d7740bd8b Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:17:56 +0000 Subject: [PATCH 62/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a3577a4bdc5009ac0875eed3158c6e75955feb99edb02af04428e5b0630f754f From 036ad155b07ac5c8f5553189d623fc3b887306ae Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:31:45 +0000 Subject: [PATCH 63/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a85cc3b02713d17c4cc7f9c897b0380270e442e2aefd57acd1e2a783fae7a9fa From 4def46330d85216d294f2f47f52409e49ec18ed6 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:50:25 +0000 Subject: [PATCH 64/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:997450a02003dce0442a6d574f2f293beda5f28d483813b7df14b7b61a59e965 From 68ff0f0066e7bc900df107e17d0bd151ae2bff3c Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:02:18 +0000 Subject: [PATCH 65/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:31dcaaa62d8252bbf97ef4e9ea359211641044ffcc8236de5b8cdc7101f7e27e From df198fd2b49a9b3564e2c9365cd1da12bc336bfb Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:26:54 +0000 Subject: [PATCH 66/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:b3f3f3460246c925b5ac870461863281c2a5696cf27e14cbf91a099f17da927c From 9bf3fd3bce485c5848a0faba0349e0e33b9ba9c5 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:40:45 +0000 Subject: [PATCH 67/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:655a2894e159d82dd299190a17531896fe7a6a7464b2ae4abfafa9a08a8bf444 From d8819d680cb8a57e5d7ca58b913dcef4d7e353c5 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:07:05 +0000 Subject: [PATCH 68/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:4e47affc2996b0c4df24217b7ec50d667b868c9151b30980275cbdbd8b732a63 --- .github/inditextech-ci-sync-manifest.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json index 11930fb..c20074f 100644 --- a/.github/inditextech-ci-sync-manifest.json +++ b/.github/inditextech-ci-sync-manifest.json @@ -1,5 +1,9 @@ { "configuration": { + "branch_roles": { + "development_branch": "develop", + "release_branch": "main" + }, "development_flow": "git-flow", "outputs": [ "publish" @@ -13,7 +17,7 @@ "creation_year": 2026, "integrity": { "algorithm": "hmac-sha256", - "signature": "b9d23187f5e79e153c11a968190ef262052db1a09dbfd3df78798ca1bb12d6ff" + "signature": "bc1a5decbd6355d73abac510a2a93679aa0eeb530181c178d3a1a266f1f41302" }, "managed_by": "InditexTech CI governance", "managed_paths": { @@ -41,6 +45,8 @@ }, "managed_variables": { "DEVELOPMENT_FLOW": "git-flow", + "GIT_FLOW_DEVELOPMENT_BRANCH": "develop", + "GIT_FLOW_RELEASE_BRANCH": "main", "PROJECT_TYPE": "single", "PUBLISH_SNAPSHOT": "true", "WORKING_DIRECTORY": "." From 02dd910f8f55a30b96c8ba7561e6b717272ff41d Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 23:13:22 +0000 Subject: [PATCH 69/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:9bc922f8923c34c82ecb328427be63332b14354739aceb57d40ad3f2d7744d76 From 4e7827385853277fda17bbd21e40fc61992ba445 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 03:17:10 +0000 Subject: [PATCH 70/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:fb5690bbc942342e03d56c794e5df40e3aa65accf4ef6c29b03ca27228121d1b --- .github/inditextech-ci-sync-manifest.json | 6 +- .github/workflows/sync-to-develop.yml | 69 ++++++++++++++--------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json index c20074f..66c33c6 100644 --- a/.github/inditextech-ci-sync-manifest.json +++ b/.github/inditextech-ci-sync-manifest.json @@ -17,7 +17,7 @@ "creation_year": 2026, "integrity": { "algorithm": "hmac-sha256", - "signature": "bc1a5decbd6355d73abac510a2a93679aa0eeb530181c178d3a1a266f1f41302" + "signature": "08f8880c92dd47eae0bd216f5bcbcc163643a06f5bfcc5517498adf2f1a47217" }, "managed_by": "InditexTech CI governance", "managed_paths": { @@ -36,7 +36,7 @@ ".github/workflows/pr-verify.yml": "b39240fd362fb004c3b47cd4189cb527d9e27a7c37645bbe6324d2a757e34f7f", ".github/workflows/push-verify.yml": "49e6e9c6a7015ea59a2bc831982b2fc4bfd1d5721302f6041170b4a15b7d56e1", ".github/workflows/scorecard-analysis.yml": "9866047c5d638ee5d9246e646400cad473ce8ab0f66b444337405b8f9f324f6e", - ".github/workflows/sync-to-develop.yml": "1b2d0f33094052f771d91a7e7c29a666e8a9cee5cc5ca28e1f40168920ca2702", + ".github/workflows/sync-to-develop.yml": "6fcb3ce7a9fb55d7322cc50255c35f6244143c6cb708899902d00945adbcd148", ".tool-versions": "5e7b05edf5d8df174df5dd99d012e57666ec9923ac8506df7dc7753ba41815fd", "CODE_OF_CONDUCT.md": "ce1e7a8f68a7917d48c03f9f7aae5529367f73af0e959276e889d33ea1e8d4ab", "CONTRIBUTING.md": "4e1264ca54a45df44b362c7533f0eba912bfa77b6f561121ae9e5e9d6aa00df3", @@ -56,7 +56,7 @@ "retained_paths": {}, "schema_version": 2, "source_digests": { - "base": "84ae844059a60d9ea8bc96910d88c912b9875aa90e48411b243f119f584fd571", + "base": "e23b6d1e03f8948e8ccc602e054fad71e0d64ff0ddeee534f915babf107edea3", "node": "d35cb892d1e48292852cdb39e10a31b4583849fd4aeb71c2e4434c7e37c4e82e" } } diff --git a/.github/workflows/sync-to-develop.yml b/.github/workflows/sync-to-develop.yml index 344251a..9c06104 100644 --- a/.github/workflows/sync-to-develop.yml +++ b/.github/workflows/sync-to-develop.yml @@ -17,10 +17,6 @@ jobs: if: >- github.event.pull_request.merged && vars.DEVELOPMENT_FLOW == 'git-flow' && - ( - github.event.pull_request.base.ref == github.event.repository.default_branch || - startsWith(github.event.pull_request.base.ref, format('{0}-', github.event.repository.default_branch)) - ) && !contains(join(github.event.pull_request.labels.*.name, ','), 'release-type') runs-on: ubuntu-24.04 timeout-minutes: 15 @@ -30,11 +26,47 @@ jobs: env: SYNC_BASE_REF: ${{ github.event.pull_request.base.ref }} steps: + - name: Resolve sync context + id: context + env: + BASE_BRANCH: ${{ github.event.pull_request.base.ref }} + DEVELOPMENT_BRANCH: ${{ vars.GIT_FLOW_DEVELOPMENT_BRANCH }} + RELEASE_BRANCH: ${{ vars.GIT_FLOW_RELEASE_BRANCH }} + run: | + set -euo pipefail + + if [[ -z "$RELEASE_BRANCH" || -z "$DEVELOPMENT_BRANCH" ]]; then + echo "::error title=Missing git-flow branch roles::GIT_FLOW_RELEASE_BRANCH and GIT_FLOW_DEVELOPMENT_BRANCH are required." + exit 1 + fi + + if [[ "$BASE_BRANCH" == "$RELEASE_BRANCH" ]]; then + develop_branch="$DEVELOPMENT_BRANCH" + elif [[ "$BASE_BRANCH" == "$RELEASE_BRANCH"-* ]]; then + suffix="${BASE_BRANCH#"$RELEASE_BRANCH"-}" + if [[ -z "$suffix" ]]; then + echo "::notice title=Sync skipped::$BASE_BRANCH has no release suffix." + echo "should_sync=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + develop_branch="$DEVELOPMENT_BRANCH-$suffix" + else + echo "::notice title=Sync skipped::$BASE_BRANCH is not a configured release branch." + echo "should_sync=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + { + echo "develop_branch=$develop_branch" + echo "should_sync=true" + } >> "$GITHUB_OUTPUT" + # The checkout runs *before* any App credential exists. This event carries # the base repository's secrets, so ordering is the cheap half of the # defence: content that lands in the workspace cannot reach a credential # that has not been minted yet. - name: Checkout the exact merged revision + if: steps.context.outputs.should_sync == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -45,6 +77,7 @@ jobs: ref: ${{ env.SYNC_BASE_REF }} - name: Create GitHub App token + if: steps.context.outputs.should_sync == 'true' id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: @@ -60,35 +93,17 @@ jobs: - name: Prepare sync branch id: prepare + if: steps.context.outputs.should_sync == 'true' env: APP_SLUG: ${{ steps.app-token.outputs.app-slug }} BASE_BRANCH: ${{ github.event.pull_request.base.ref }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + DEVELOP_BRANCH: ${{ steps.context.outputs.develop_branch }} GH_TOKEN: ${{ steps.app-token.outputs.token }} run: | set -euo pipefail - if [[ "$BASE_BRANCH" == "$DEFAULT_BRANCH" ]]; then - develop_branch=develop - elif [[ "$BASE_BRANCH" == "$DEFAULT_BRANCH"-* ]]; then - suffix="${BASE_BRANCH#"$DEFAULT_BRANCH"-}" - if [[ -z "$suffix" ]]; then - echo "::notice title=Sync skipped::$BASE_BRANCH has no release suffix." - echo "should_sync=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - develop_branch="develop-$suffix" - else - echo "::notice title=Sync skipped::$BASE_BRANCH is not a default-branch release role." - echo "should_sync=false" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [[ "$BASE_BRANCH" == "$develop_branch" ]]; then - echo "::notice title=Sync skipped::The source branch is already the development branch." - echo "should_sync=false" >> "$GITHUB_OUTPUT" - exit 0 - fi + # shellcheck disable=SC2153 + develop_branch="$DEVELOP_BRANCH" sync_branch="automated/sync-from-${BASE_BRANCH}-to-${develop_branch}" @@ -134,7 +149,7 @@ jobs: if: steps.prepare.outputs.should_sync == 'true' env: BASE_BRANCH: ${{ github.event.pull_request.base.ref }} - DEVELOP_BRANCH: ${{ steps.prepare.outputs.develop_branch }} + DEVELOP_BRANCH: ${{ steps.context.outputs.develop_branch }} GH_TOKEN: ${{ steps.app-token.outputs.token }} SOURCE_PULL_REQUEST: ${{ github.event.pull_request.number }} SYNC_BRANCH: ${{ steps.prepare.outputs.sync_branch }} From 35a5646abfed3da92ce6346b791d7c1ded19c685 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 04:32:33 +0000 Subject: [PATCH 71/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:ee62607f88bbdf83108f83dcdf7f3245c32205c56ffa7bb855836591fb0b4bd9 From b2ef7224aa8786c19227d49b4b7b84d4d3c5dd0b Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:09:13 +0000 Subject: [PATCH 72/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:fc20be6b0a93e322578242d012a7cd494f01b27bb029a6a104490549183d016e From 62e4b48fdfbfda9f3b7d93bf3f270d3ab8a93f1e Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:23:51 +0000 Subject: [PATCH 73/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:7f0bc8e6e5d2bcffaafc3eeb3b4be369d09cee80625ceb091d711509943c6547 From 649862bc1361a28de16f3307eed109946e7c6997 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:01:09 +0000 Subject: [PATCH 74/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:0cf5ef5b81cb282b7b835a4739490ae79b16be54935b2abe44f4c8191ba32db7 From b23307f492760d0f1d5308676633c18d59ab6577 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:15:11 +0000 Subject: [PATCH 75/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:a522d968b14d23027ed5d96cda7e365f570af27aff00432b60d3f92e45f1e34b From 6cab15eca8f354d30a41847d88fa90731bfec41a Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:23:37 +0000 Subject: [PATCH 76/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:06b277dfc48d60fcb56bca77d74e6fe16c1efc3617263a467a54230980499154 From 17f6ea4ffe71803c86a688ac4db2d9d9a0cbf822 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:37:57 +0000 Subject: [PATCH 77/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:bc12ca5edac094bb1ef3d4987bbf066dab3183449200b740e24f7c1895a5f030 From f072c190c2bfbbebd4e6cc004921b3d4856fbba3 Mon Sep 17 00:00:00 2001 From: "inditextechci-sync[bot]" <312492039+inditextechci-sync[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:44:51 +0000 Subject: [PATCH 78/78] chore(ci-governance): synchronize Node profile Governance-Provenance: hmac-sha256:4361cf96d4e3131d93ac530ba93e61e7c38837d0283e190453739ab62c9d6aab