diff --git a/.github/ISSUE_TEMPLATE/1-bug.yml b/.github/ISSUE_TEMPLATE/1-bug.yml new file mode 100644 index 00000000..b03cd595 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug.yml @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 +name: Bug Report +description: Report a reproducible bug +title: '[Bug] ' +labels: ['bug'] +assignees: [] + +body: + - type: textarea + id: description + attributes: + label: Description + description: What's the problem? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: How can we reproduce the bug? + placeholder: | + 1. Go to ... + 2. Run ... + 3. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + validations: + required: false + + - type: input + id: version + attributes: + label: Version / Environment + placeholder: "e.g. v0.3.2, Node 18, macOS" + validations: + required: false + + - type: textarea + id: notes + attributes: + label: Additional context or logs + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/2-feature.yml b/.github/ISSUE_TEMPLATE/2-feature.yml new file mode 100644 index 00000000..b44bd15e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 +name: Feature Request +description: Suggest a feature or improvement +title: '[Feature] ' +labels: ['enhancement'] +assignees: [] + +body: + - type: textarea + id: proposal + attributes: + label: Feature description + description: What would you like to see added or changed? + validations: + required: true + + - type: textarea + id: motivation + attributes: + label: Use case or motivation + description: Why is this feature useful? + + - type: dropdown + id: contribution + attributes: + label: Would you like to work on this? + options: + - Yes, I'd like to open a PR + - Maybe, I'd need help + - No, I'm just proposing it + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/3-other.yml b/.github/ISSUE_TEMPLATE/3-other.yml new file mode 100644 index 00000000..69681993 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-other.yml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 +name: Other +description: Use this for anything that doesn't fit the other templates +title: '[Other] ' +labels: ['discussion'] +assignees: [] + +body: + - type: textarea + id: context + attributes: + label: Context or topic + description: Briefly explain what this is about — question, idea, feedback, etc. + validations: + required: true + + - type: textarea + id: details + attributes: + label: Details or background + description: Add any supporting info, links, logs, or notes that may be useful. + placeholder: | + Example: + - I'm wondering if we should improve X + - This tool might be useful for Y + - We discussed this in meeting Z + + - type: dropdown + id: nextstep + attributes: + label: What are you hoping to do next? + options: + - Just opening this for awareness + - Looking for input before starting work + - Need help or advice + - Happy to open a PR based on feedback diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..30e40d31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 +blank_issues_enabled: false diff --git a/.github/inditextech-ci-java.json b/.github/inditextech-ci-java.json new file mode 100644 index 00000000..8ec416bf --- /dev/null +++ b/.github/inditextech-ci-java.json @@ -0,0 +1,17 @@ +{ + "project_type": "monorepo", + "release": { + "lifecycle": "delegated", + "packages": [ + "karatetools-clients", + "karatetools-generators", + "karatetools-runner", + "karatetools-starter", + "karatetools-boot", + "karatetools-archetype" + ], + "strategy": "locked-step" + }, + "schema_version": 1, + "working_directory": "code" +} diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json new file mode 100644 index 00000000..eddccd45 --- /dev/null +++ b/.github/inditextech-ci-sync-manifest.json @@ -0,0 +1,69 @@ +{ + "configuration": { + "development_flow": "git-flow", + "outputs": [ + "gh-release", + "publish" + ], + "parameters": { + "project_type": "monorepo", + "release": { + "lifecycle": "delegated", + "packages": [ + "karatetools-clients", + "karatetools-generators", + "karatetools-runner", + "karatetools-starter", + "karatetools-boot", + "karatetools-archetype" + ], + "strategy": "locked-step" + }, + "working_directory": "code" + } + }, + "creation_year": 2026, + "integrity": { + "algorithm": "hmac-sha256", + "signature": "e5cca9ba37d26fa7f61668b3ccdb6c411dab96fc10ada3c3eedeeb85340bf293" + }, + "managed_by": "InditexTech CI governance", + "managed_paths": { + ".github/ISSUE_TEMPLATE/1-bug.yml": "cd4dad3f32fe707e06324790a2fe2c25829c55566de2b459d744e785b36e1205", + ".github/ISSUE_TEMPLATE/2-feature.yml": "ebb92730d6c01641d055f97ac094cbe07f88fac24be9b689ddf26e47e7bda863", + ".github/ISSUE_TEMPLATE/3-other.yml": "5a839b39a98704036210dee498146f51bd5a6c057228685680e02004ff063a9a", + ".github/ISSUE_TEMPLATE/config.yml": "10e5dee4d49aa9e7792ceb4c986160c61f95da1ba00a64bf06e5f1f636cd17ec", + ".github/inditextech-ci-java.json": "776afda68f8d2ad8d17e236d2b824a5043d26fe0e00158d57156bf088ad61e26", + ".github/workflows/code-maven_java-build_snapshot.yml": "1f6201f3449678c88360797c201fc3c4d9c092bdd40d0d2a6f312222484d2227", + ".github/workflows/code-maven_java-release-core.yml": "5b1d6e048f34e866b95b7b4a2726260701bedbe956a721fd855c7c0f9948fcfe", + ".github/workflows/codeql.yml": "b4ef9dfddd7b28387b669d48a4228225dc1d6dc76f98d21b65dd4a324d228ad5", + ".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", + "repolinter.json": "0efb305c47a63f03c488a34906fc2d5471c84145c2ff553f2aacf3a41b92e1a1" + }, + "managed_variables": { + "DEVELOPMENT_FLOW": "git-flow", + "PROJECT_TYPE": "monorepo", + "WORKING_DIRECTORY": "code" + }, + "profile": "java", + "repository_id": 897762003, + "retained_paths": { + ".github/PULL_REQUEST_TEMPLATE.md": "09f1478b6dfe5eb853c5f494f2f8e5bb2a3bc0c3170ffb87a3133f0d6a4ee344", + ".github/workflows/code-maven_java-PR_verify.yml": "b1d78b1759aba9cf1c6ea4b6e1c6c8adada42649ea463ed18c495d48a218b486", + ".github/workflows/code-maven_java-release.yml": "fed5861728b07ea63e3c3f83d2de2013d94efc1b8ea951271147b36e25885e2f", + ".github/workflows/code-maven_java-sonarcloud-analysis.yml": "d9c9f379369a1f2f6c660e5ee32bafd5ace0a689e1124263edf27e7becd4ee06", + ".github/workflows/code-release_preview.yml": "36e40ae0d83b4cbb7d5c41f8112a5b7aa6c0ab9aa085f075c0ba152851fee202", + "CODE_OF_CONDUCT.md": "0e44a451537d6013b1c87a9d57e1bf3cc4cf11521fee28f6192e4c3d84c723e6", + "CONTRIBUTING.md": "4d29d7ccdce4f14875570ef08a4211fe6660e2289d61d97a4984fcc86e7120be", + "SECURITY.md": "115fde93b8614d527747720b23a77c7c9a794fea371aa3c937aa8ecc269e8b42", + "code/.tool-versions": "d06a804e96eb8443f7bc3f134a7091eab2ebb59fbea1fb24961d0dcf238ad978" + }, + "schema_version": 2, + "source_digests": { + "base": "330cbba1e08096bd0792b772e00b4745d3bf429c41c889bf0653bc979ef31172", + "java": "1745af962302c8ee00d2e079397ac761c612bcc0e8d8614059b5f92c5a288d29" + } +} diff --git a/.github/workflows/code-maven_java-build_snapshot.yml b/.github/workflows/code-maven_java-build_snapshot.yml new file mode 100644 index 00000000..5405ccba --- /dev/null +++ b/.github/workflows/code-maven_java-build_snapshot.yml @@ -0,0 +1,278 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: code-maven-java-build-snapshot +run-name: Build Java snapshot from ${{ github.event_name }} + +on: + workflow_dispatch: + push: + branches: [main, main-*, develop, develop-*] + pull_request: + types: [opened, synchronize, reopened] + branches: [main, main-*, develop, develop-*] + +permissions: + contents: read + +concurrency: + group: code-maven-java-build-snapshot-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} + ASDF_BRANCH_VERSION: '0.18.0' + +jobs: + build-snapshot: + name: Build Snapshot + if: >- + ( + vars.DEVELOPMENT_FLOW == 'trunk-based-development' && + ( + (github.event_name == 'workflow_dispatch' && (github.ref_name == 'main' || startsWith(github.ref_name, 'main-'))) || + (github.event_name == 'pull_request' && (github.event.pull_request.base.ref == 'main' || startsWith(github.event.pull_request.base.ref, 'main-'))) || + (github.event_name == 'push' && (github.ref_name == 'main' || startsWith(github.ref_name, 'main-'))) + ) + ) || + ( + vars.DEVELOPMENT_FLOW == 'git-flow' && + ( + (github.event_name == 'workflow_dispatch' && (github.ref_name == 'develop' || startsWith(github.ref_name, 'develop-'))) || + (github.event_name == 'pull_request' && (github.event.pull_request.base.ref == 'develop' || startsWith(github.event.pull_request.base.ref, 'develop-'))) || + (github.event_name == 'push' && (github.ref_name == 'develop' || startsWith(github.ref_name, 'develop-'))) + ) + ) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + outputs: + publish_snapshot: ${{ steps.build.outputs.publish_snapshot }} + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Resolve governed project type + shell: bash + run: | + set -euo pipefail + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + if [[ ! -f "$descriptor" ]]; then + echo "::error title=Missing descriptor::.github/inditextech-ci-java.json not found." + exit 1 + fi + project_type="$(jq -r '.project_type // empty' "$descriptor")" + case "$project_type" in + single|monorepo) ;; + *) + echo "::error title=Invalid project type::Descriptor .project_type must be single or monorepo." + exit 1 + ;; + esac + case "${WORKING_DIRECTORY:-}" in + ""|/*|..|../*|*/..|*/../*) + echo "::error title=Invalid working directory::WORKING_DIRECTORY must be a non-empty relative repository path." >&2 + exit 1 + ;; + esac + if [[ ! -d "$WORKING_DIRECTORY" ]]; then + echo "::error title=Missing working directory::'${WORKING_DIRECTORY}' is not a directory in this checkout." >&2 + exit 1 + fi + echo "PROJECT_TYPE=$project_type" >> "$GITHUB_ENV" + + - name: Set up workflow Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '25.0.2+10.0.LTS' + cache: maven + cache-dependency-path: ${{ env.WORKING_DIRECTORY }}/**/pom.xml + + - 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 Java + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.0 + with: + tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} + asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + + - name: Resolve build JDK from .tool-versions + id: jdk + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + echo "java_home=$(asdf where java)" >> "$GITHUB_OUTPUT" + + - name: Verify snapshot candidate + id: build + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + run: | + set -euo pipefail + + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + snapshot_optin="$(jq -r '.release.snapshot_publish // false' "$descriptor")" + + publish_snapshot=false + if [[ "$snapshot_optin" == "true" && "$GITHUB_EVENT_NAME" == "push" ]]; then + committer_email="$(git -C "$GITHUB_WORKSPACE" show -s --format=%ae HEAD)" + if [[ "$committer_email" == *"[bot]@users.noreply.github.com" ]]; then + echo "::notice title=Snapshot skipped::Release-bot commit does not need a snapshot." + else + publish_snapshot=true + fi + fi + echo "publish_snapshot=$publish_snapshot" >> "$GITHUB_OUTPUT" + + case "$PROJECT_TYPE" in + single|monorepo) + mvn -B --no-transfer-progress -f pom.xml clean verify + ;; + *) + echo "::error title=Invalid project type::PROJECT_TYPE must be single or monorepo." + exit 1 + ;; + esac + + publish-snapshot: + name: Publish snapshot to Maven Central + needs: build-snapshot + if: github.event_name == 'push' && needs.build-snapshot.outputs.publish_snapshot != 'false' + runs-on: ubuntu-24.04 + timeout-minutes: 30 + environment: maven-central + permissions: + contents: read + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + CI_GPG_SECRET_KEY: ${{ secrets.CI_GPG_SECRET_KEY }} + CI_GPG_SECRET_KEY_PASSWORD: ${{ secrets.CI_GPG_SECRET_KEY_PASSWORD }} + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Resolve governed project type + shell: bash + run: | + set -euo pipefail + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + if [[ ! -f "$descriptor" ]]; then + echo "::error title=Missing descriptor::.github/inditextech-ci-java.json not found." + exit 1 + fi + project_type="$(jq -r '.project_type // empty' "$descriptor")" + case "$project_type" in + single|monorepo) ;; + *) + echo "::error title=Invalid project type::Descriptor .project_type must be single or monorepo." + exit 1 + ;; + esac + case "${WORKING_DIRECTORY:-}" in + ""|/*|..|../*|*/..|*/../*) + echo "::error title=Invalid working directory::WORKING_DIRECTORY must be a non-empty relative repository path." >&2 + exit 1 + ;; + esac + if [[ ! -d "$WORKING_DIRECTORY" ]]; then + echo "::error title=Missing working directory::'${WORKING_DIRECTORY}' is not a directory in this checkout." >&2 + exit 1 + fi + echo "PROJECT_TYPE=$project_type" >> "$GITHUB_ENV" + + - name: Set up workflow Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '25.0.2+10.0.LTS' + cache: maven + cache-dependency-path: ${{ env.WORKING_DIRECTORY }}/**/pom.xml + + - 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 Java + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.0 + with: + tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} + asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + + - name: Resolve build JDK from .tool-versions + id: jdk + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + echo "java_home=$(asdf where java)" >> "$GITHUB_OUTPUT" + + - name: Resolve publishable modules + id: modules + shell: bash + run: | + set -euo pipefail + if [[ "$PROJECT_TYPE" != "monorepo" ]]; then + echo "packages=" >> "$GITHUB_OUTPUT" + exit 0 + fi + + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + strategy="$(jq -er '.release.strategy' "$descriptor")" + if [[ "$strategy" == "locked-step" ]]; then + echo "packages=" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [[ "$strategy" != "independent" ]]; then + echo "::error title=Invalid release strategy::Monorepos must declare release.strategy as locked-step or independent." + exit 1 + fi + echo "::error title=Unsupported independent snapshot::Independent Maven monorepo snapshots require InditexTech/gh-actions#10." + exit 1 + + - name: Deploy snapshot to Maven Central + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + uses: InditexTech/gh-actions/maven-central@7138f88b2fa8e90318bcb4bf5f9ce6c147b13d2f # v1.0.0 + with: + working-directory: ${{ vars.WORKING_DIRECTORY }} + project-type: ${{ env.PROJECT_TYPE }} + packages: ${{ steps.modules.outputs.packages }} + auto-publish: 'false' diff --git a/.github/workflows/code-maven_java-release-core.yml b/.github/workflows/code-maven_java-release-core.yml new file mode 100644 index 00000000..e8a53767 --- /dev/null +++ b/.github/workflows/code-maven_java-release-core.yml @@ -0,0 +1,1321 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: code-maven-java-release-core + +on: + workflow_call: + inputs: + release_type: + description: Optional release type supplied by the fixed release workflow. + required: false + type: string + default: '' + outputs: + release_commit: + description: Immutable commit used to build the release artifacts. + value: ${{ jobs.prepare-release.outputs.release_commit }} + releases: + description: JSON array of release tags, versions, module lists, and next-dev versions. + value: ${{ jobs.prepare-release.outputs.releases }} + secrets: + APP_PRIVATE_KEY: + required: true + +permissions: + actions: read + contents: read + +env: + CI_GOVERNANCE_RELEASE_STAGING_REF: refs/ci-governance/release/${{ github.run_id }}-${{ github.run_attempt }} + CI_GOVERNANCE_RELEASE_TAG_STAGING_REF: refs/ci-governance/release-tag/${{ github.run_id }}-${{ github.run_attempt }} + WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} + ASDF_BRANCH_VERSION: '0.18.0' + +jobs: + prepare-release: + name: Prepare Release + if: >- + ( + 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 }} + promote_commit: ${{ steps.release-metadata.outputs.promote_commit }} + releases: ${{ steps.release-metadata.outputs.releases }} + expected_ref: ${{ steps.release-metadata.outputs.expected_ref }} + lifecycle_mode: ${{ steps.lifecycle.outputs.mode }} + staging_ref: ${{ steps.release-metadata.outputs.staging_ref }} + staged_tag_ref: ${{ steps.release-metadata.outputs.staged_tag_ref }} + release_source_artifact: ${{ steps.release-source-handoff.outputs.artifact_name }} + release_source_sha256: ${{ steps.release-source-handoff.outputs.bundle_sha256 }} + steps: + - name: Resolve release context + id: release-context + env: + EVENT_SHA: ${{ github.sha }} + PR_BASE_BRANCH: ${{ github.event.pull_request.base.ref }} + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + PR_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + REQUESTED_RELEASE_TYPE: ${{ inputs.release_type }} + shell: bash + run: | + set -euo pipefail + + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + baseline_branch="$GITHUB_REF_NAME" + release_labels="$REQUESTED_RELEASE_TYPE" + release_ref="$EVENT_SHA" + else + baseline_branch="$PR_BASE_BRANCH" + release_labels="$PR_LABELS" + release_ref="$PR_MERGE_SHA" + fi + + if [[ -z "$baseline_branch" || ! "$release_ref" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error title=Invalid release context::A baseline branch and immutable release commit are required." + exit 1 + fi + + { + echo "baseline_branch=$baseline_branch" + echo "release_labels=$release_labels" + 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 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + + - name: Check out immutable release source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ steps.release-context.outputs.release_ref }} + token: ${{ steps.app-token.outputs.token }} + + - name: Resolve governed project type + shell: bash + run: | + set -euo pipefail + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + if [[ ! -f "$descriptor" ]]; then + echo "::error title=Missing descriptor::.github/inditextech-ci-java.json not found." + exit 1 + fi + project_type="$(jq -r '.project_type // empty' "$descriptor")" + case "$project_type" in + single|monorepo) ;; + *) + echo "::error title=Invalid project type::Descriptor .project_type must be single or monorepo." + exit 1 + ;; + esac + case "${WORKING_DIRECTORY:-}" in + ""|/*|..|../*|*/..|*/../*) + echo "::error title=Invalid working directory::WORKING_DIRECTORY must be a non-empty relative repository path." >&2 + exit 1 + ;; + esac + if [[ ! -d "$WORKING_DIRECTORY" ]]; then + echo "::error title=Missing working directory::'${WORKING_DIRECTORY}' is not a directory in this checkout." >&2 + exit 1 + fi + echo "PROJECT_TYPE=$project_type" >> "$GITHUB_ENV" + + - name: Resolve release lifecycle + id: lifecycle + shell: bash + run: | + set -euo pipefail + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + mode="$(jq -r '.release.lifecycle // "govern-inject"' "$descriptor")" + case "$mode" in + govern-inject|delegated) ;; + *) + echo "::error title=Invalid release lifecycle::release.lifecycle must be govern-inject or delegated." + exit 1 + ;; + esac + echo "mode=$mode" >> "$GITHUB_OUTPUT" + + - name: Set up workflow Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '25.0.2+10.0.LTS' + cache: maven + cache-dependency-path: ${{ env.WORKING_DIRECTORY }}/**/pom.xml + + - 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 Java + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.0 + with: + tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} + asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + + - name: Resolve build JDK from .tool-versions + id: jdk + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + echo "java_home=$(asdf where java)" >> "$GITHUB_OUTPUT" + + - name: Verify release candidate + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + run: | + set -euo pipefail + case "$PROJECT_TYPE" in + single|monorepo) + mvn -B --no-transfer-progress -f pom.xml clean verify + ;; + *) + echo "::error title=Invalid project type::PROJECT_TYPE must be single or monorepo." + exit 1 + ;; + esac + + - name: Plan release metadata + id: release-plan + if: steps.lifecycle.outputs.mode != 'delegated' + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + 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_REF: ${{ steps.release-context.outputs.release_ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + + gh auth setup-git + + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + records_file="$RUNNER_TEMP/java-release-records.jsonl" + modules_file="$RUNNER_TEMP/java-release-modules.txt" + source_commit="$(git rev-parse "$RELEASE_REF^{commit}")" + expected_ref="refs/heads/$BASELINE_BRANCH" + remote_line="$(git ls-remote --exit-code origin "$expected_ref")" + read -r remote_commit remote_ref extra <<< "$remote_line" + + if [[ "$remote_ref" != "$expected_ref" || -n "${extra:-}" ]]; then + echo "::error title=Release branch verification failed::Could not resolve exactly one destination branch." + exit 1 + fi + + resume="false" + release_commit="" + if [[ "$remote_commit" != "$source_commit" ]]; then + git fetch --no-tags origin "$remote_commit" + release_commit="$(git rev-list --ancestry-path --reverse "${source_commit}..${remote_commit}" | head -1)" + if [[ -z "$release_commit" ]] \ + || [[ "$(git rev-parse "${release_commit}^")" != "$source_commit" ]] \ + || [[ "$(git show -s --format=%s "$release_commit")" != "[java-release] Prepare release" ]]; then + echo "::error title=Release branch advanced::The destination branch moved after the release source commit." + exit 1 + fi + git checkout --detach "$release_commit" + resume="true" + fi + + case ",$RELEASE_LABELS," in + *",release-type/major,"*) release_bump="major" ;; + *",release-type/minor,"*) release_bump="minor" ;; + *",release-type/hotfix,"*|*",release-type/multi-hotfix,"*|*",release-type/patch,"*) + release_bump="patch" + ;; + *) + if [[ "$DEVELOPMENT_FLOW" == "trunk-based-development" ]]; then + release_bump="minor" + else + echo "::error title=Missing release type::git-flow releases require a release-type label." + exit 1 + fi + ;; + esac + + artifact_id() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' + } + strip_snapshot() { printf '%s' "${1%-SNAPSHOT}"; } + # Resolve declared Maven artifactIds through the reactor. + read_version() { + if [[ -n "$1" ]]; then + mvn -q -B --no-transfer-progress -f pom.xml -pl ":$1" \ + help:evaluate -Dexpression=project.version -DforceStdout + else + mvn -q -B --no-transfer-progress -f pom.xml \ + help:evaluate -Dexpression=project.version -DforceStdout + fi + } + read_artifact() { + if [[ -n "$1" ]]; then + mvn -q -B --no-transfer-progress -f pom.xml -pl ":$1" \ + help:evaluate -Dexpression=project.artifactId -DforceStdout + else + mvn -q -B --no-transfer-progress -f pom.xml \ + help:evaluate -Dexpression=project.artifactId -DforceStdout + fi + } + bump_semver() { + local major minor patch + IFS=. read -r major minor patch <<< "$1" + case "$2" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + esac + printf '%s.%s.%s' "$major" "$minor" "$patch" + } + set_version() { + if [[ -n "$2" ]]; then + mvn -q -B --no-transfer-progress -f pom.xml versions:set \ + -DgroupId=com.inditextech -DartifactId="$2" -DoldVersion='*' \ + -DnewVersion="$1" -DprocessAllModules=false -DgenerateBackupPoms=false + elif [[ "$PROJECT_TYPE" == "monorepo" ]]; then + mvn -q -B --no-transfer-progress -f pom.xml versions:set \ + -DnewVersion="$1" -DprocessAllModules=true -DgenerateBackupPoms=false + else + mvn -q -B --no-transfer-progress -f pom.xml versions:set \ + -DnewVersion="$1" -DgenerateBackupPoms=false + fi + } + + if [[ "$PROJECT_TYPE" == "single" ]]; then + release_strategy=single + printf '%s\n' "$(read_artifact "")" > "$modules_file" + elif [[ "$PROJECT_TYPE" == "monorepo" ]]; then + release_strategy="$(jq -er '.release.strategy' "$descriptor")" + : > "$modules_file" + while IFS= read -r package; do + printf '%s\n' "$(artifact_id "$package")" >> "$modules_file" + done < <(jq -er '.release.packages[]' "$descriptor") + else + echo "::error title=Invalid project type::PROJECT_TYPE must be single or monorepo." + exit 1 + fi + + write_records() { + : > "$records_file" + if [[ "$release_strategy" == "single" ]]; then + artifact="$(<"$modules_file")" + version="$(strip_snapshot "$(read_version "")")" + next_dev="$(bump_semver "$version" "$release_bump")-SNAPSHOT" + jq -cn \ + --arg artifact "$artifact" \ + --arg version "$version" \ + --arg tag "$version" \ + --arg next_dev "$next_dev" \ + '{packages: [$artifact], version: $version, tag: $tag, next_dev: $next_dev}' >> "$records_file" + return + fi + + if [[ "$release_strategy" == "locked-step" ]]; then + modules=() + while IFS= read -r module; do + modules+=("$module") + done < "$modules_file" + versions=() + for module in "${modules[@]}"; do + versions+=("$(strip_snapshot "$(read_version "$module")")") + done + if [[ "$(printf '%s\n' "${versions[@]}" | sort -u | wc -l | tr -d ' ')" -ne 1 ]]; then + echo "::error title=Invalid locked-step reactor::All release modules must share the same version." + exit 1 + fi + version="${versions[0]}" + next_dev="$(bump_semver "$version" "$release_bump")-SNAPSHOT" + jq -cn \ + --arg version "$version" \ + --arg tag "$version" \ + --arg next_dev "$next_dev" \ + '{packages: [], version: $version, tag: $tag, next_dev: $next_dev}' >> "$records_file" + return + fi + + while IFS= read -r module; do + version="$(strip_snapshot "$(read_version "$module")")" + next_dev="$(bump_semver "$version" "$release_bump")-SNAPSHOT" + jq -cn \ + --arg module "$module" \ + --arg version "$version" \ + --arg tag "${module}-${version}" \ + --arg next_dev "$next_dev" \ + '{packages: [$module], version: $version, tag: $tag, next_dev: $next_dev}' >> "$records_file" + done < "$modules_file" + } + + if [[ "$resume" == "false" ]]; then + write_records + releases="$(jq -cs . "$records_file")" + + if [[ "$release_strategy" == "independent" ]]; then + while IFS= read -r record; do + artifact="$(jq -r '.packages[0]' <<< "$record")" + version="$(jq -r '.version' <<< "$record")" + set_version "$version" "$artifact" + done < <(jq -c '.[]' <<< "$releases") + else + set_version "$(jq -r '.[0].version' <<< "$releases")" "" + fi + + while IFS= read -r tag; do + if git show-ref --tags --verify --quiet "refs/tags/$tag"; then + echo "::error title=Tag conflict::$tag already exists." + exit 1 + fi + done < <(jq -r '.[].tag' <<< "$releases") + + git -C "$GITHUB_WORKSPACE" add --update -- "$WORKING_DIRECTORY" + if git diff --cached --quiet; then + echo "::error title=Missing release metadata::Versioning did not produce changes to commit." + exit 1 + fi + + primary_version="$(jq -r '.[0].version' <<< "$releases")" + echo "RELEASE_VERSION=$primary_version" >> "$GITHUB_ENV" + echo "RELEASE_BUMP=$release_bump" >> "$GITHUB_ENV" + else + write_records + releases="$(jq -cs . "$records_file")" + head_commit="$(git rev-parse HEAD)" + while IFS= read -r tag; do + if [[ "$(git rev-list -n 1 "$tag")" != "$head_commit" ]]; then + echo "::error title=Incomplete release resume::$tag does not point to the release commit." + exit 1 + fi + done < <(jq -r '.[].tag' <<< "$releases") + fi + + { + echo "resume=$resume" + echo "expected_ref=$expected_ref" + echo "releases_intermediate<> "$GITHUB_OUTPUT" + + - name: Bump CHANGELOG for release + if: steps.release-plan.outputs.resume != 'true' && steps.lifecycle.outputs.mode != 'delegated' + uses: release-flow/keep-a-changelog-action@74931dec7ecdbfc8e38ac9ae7e8dd84c08db2f32 # v3.0.0 + with: + command: bump + version: ${{ env.RELEASE_BUMP }} + changelog: ${{ env.WORKING_DIRECTORY }}/CHANGELOG.md + fail-on-empty-release-notes: false + keep-unreleased-section: true + tag-prefix: "" + + - name: Reconcile CHANGELOG version with release + if: steps.release-plan.outputs.resume != 'true' && steps.lifecycle.outputs.mode != 'delegated' + env: + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + cl="CHANGELOG.md" + cl_ver="$(grep -m1 -E '^## \[[0-9]' "$cl" | sed -E 's/^## \[([^]]+)\].*/\1/')" + if [[ -z "$cl_ver" ]]; then + echo "::error title=CHANGELOG has no release entry::Expected a '## [X.Y.Z]' heading after the bump step." + exit 1 + fi + if [[ "$cl_ver" != "$RELEASE_VERSION" ]]; then + echo "::notice title=CHANGELOG version reconciled::keep-a-changelog wrote ${cl_ver}; re-anchoring to release ${RELEASE_VERSION}." + cl_ver_re="${cl_ver//./\\.}" + tmp="$(mktemp)" + sed -E \ + -e "s@^## \[${cl_ver_re}\]( - )@## [${RELEASE_VERSION}]\1@" \ + -e "s@^(\[Unreleased\]: .*/compare/)${cl_ver_re}(\.\.\.HEAD)@\1${RELEASE_VERSION}\2@" \ + -e "s@^\[${cl_ver_re}\]: (.*/)(compare/[^ ]*\.\.\.|releases/tag/)${cl_ver_re}@[${RELEASE_VERSION}]: \1\2${RELEASE_VERSION}@" \ + "$cl" > "$tmp" + cat "$tmp" > "$cl" + rm -f "$tmp" + fi + + - name: Commit and stage release + if: steps.release-plan.outputs.resume != 'true' && steps.lifecycle.outputs.mode != 'delegated' + id: release-commit + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASES: ${{ steps.release-plan.outputs.releases_intermediate }} + EXPECTED_REF: ${{ steps.release-plan.outputs.expected_ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + git -C "$GITHUB_WORKSPACE" add -- "$WORKING_DIRECTORY/CHANGELOG.md" + git -C "$GITHUB_WORKSPACE" add --update -- "$WORKING_DIRECTORY" + if git diff --cached --quiet; then + echo "::error title=Missing release metadata::Versioning did not produce changes to commit." + exit 1 + fi + git config user.name "${APP_SLUG}[bot]" + git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + git commit -m "[java-release] Prepare release" + + while IFS= read -r record; do + git check-ref-format --allow-onelevel "refs/tags/$(jq -r '.tag' <<< "$record")" + done < <(jq -c '.[]' <<< "$RELEASES") + + git push --atomic --force origin "HEAD:$CI_GOVERNANCE_RELEASE_STAGING_REF" + + - name: Plan delegated release + id: delegated-plan + if: steps.lifecycle.outputs.mode == 'delegated' + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + DEVELOPMENT_FLOW: ${{ vars.DEVELOPMENT_FLOW }} + RELEASE_LABELS: ${{ steps.release-context.outputs.release_labels }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + strip_snapshot() { printf '%s' "${1%-SNAPSHOT}"; } + bump_semver() { + local major minor patch + IFS=. read -r major minor patch <<< "$1" + case "$2" in + major) major=$((major + 1)); minor=0; patch=0 ;; + minor) minor=$((minor + 1)); patch=0 ;; + patch) patch=$((patch + 1)) ;; + esac + printf '%s.%s.%s' "$major" "$minor" "$patch" + } + case ",$RELEASE_LABELS," in + *",release-type/major,"*) release_bump="major" ;; + *",release-type/minor,"*) release_bump="minor" ;; + *",release-type/hotfix,"*|*",release-type/multi-hotfix,"*|*",release-type/patch,"*) + release_bump="patch" + ;; + *) + if [[ "$DEVELOPMENT_FLOW" == "trunk-based-development" ]]; then + release_bump="minor" + else + echo "::error title=Missing release type::git-flow releases require a release-type label." + exit 1 + fi + ;; + esac + current_version="$(mvn -q -B --no-transfer-progress -f pom.xml \ + help:evaluate -Dexpression=project.version -DforceStdout)" + release_version="$(strip_snapshot "$current_version")" + next_dev="$(bump_semver "$release_version" "$release_bump")-SNAPSHOT" + git -C "$GITHUB_WORKSPACE" tag --list | sort > "$RUNNER_TEMP/java-tags-before.txt" + { + echo "release_version=$release_version" + echo "next_dev=$next_dev" + } >> "$GITHUB_OUTPUT" + echo "RELEASE_VERSION=$release_version" >> "$GITHUB_ENV" + echo "RELEASE_BUMP=$release_bump" >> "$GITHUB_ENV" + + - name: Bump CHANGELOG for delegated release + if: steps.lifecycle.outputs.mode == 'delegated' + uses: release-flow/keep-a-changelog-action@74931dec7ecdbfc8e38ac9ae7e8dd84c08db2f32 # v3.0.0 + with: + command: bump + version: ${{ env.RELEASE_BUMP }} + changelog: ${{ env.WORKING_DIRECTORY }}/CHANGELOG.md + fail-on-empty-release-notes: false + keep-unreleased-section: true + tag-prefix: "" + + - name: Reconcile CHANGELOG version with delegated release + if: steps.lifecycle.outputs.mode == 'delegated' + env: + RELEASE_VERSION: ${{ env.RELEASE_VERSION }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + cl="CHANGELOG.md" + cl_ver="$(grep -m1 -E '^## \[[0-9]' "$cl" | sed -E 's/^## \[([^]]+)\].*/\1/')" + if [[ -z "$cl_ver" ]]; then + echo "::error title=CHANGELOG has no release entry::Expected a '## [X.Y.Z]' heading after the bump step." + exit 1 + fi + if [[ "$cl_ver" != "$RELEASE_VERSION" ]]; then + echo "::notice title=CHANGELOG version reconciled::keep-a-changelog wrote ${cl_ver}; re-anchoring to release ${RELEASE_VERSION}." + cl_ver_re="${cl_ver//./\\.}" + tmp="$(mktemp)" + sed -E \ + -e "s@^## \[${cl_ver_re}\]( - )@## [${RELEASE_VERSION}]\1@" \ + -e "s@^(\[Unreleased\]: .*/compare/)${cl_ver_re}(\.\.\.HEAD)@\1${RELEASE_VERSION}\2@" \ + -e "s@^\[${cl_ver_re}\]: (.*/)(compare/[^ ]*\.\.\.|releases/tag/)${cl_ver_re}@[${RELEASE_VERSION}]: \1\2${RELEASE_VERSION}@" \ + "$cl" > "$tmp" + cat "$tmp" > "$cl" + rm -f "$tmp" + fi + + - name: Prepare delegated release + id: delegated-release + if: steps.lifecycle.outputs.mode == 'delegated' + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + BASELINE_BRANCH: ${{ steps.release-context.outputs.baseline_branch }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + NEXT_DEV: ${{ steps.delegated-plan.outputs.next_dev }} + RELEASE_VERSION: ${{ steps.delegated-plan.outputs.release_version }} + 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 -C "$GITHUB_WORKSPACE" checkout -B "$BASELINE_BRANCH" + + packages_json='[]' + if [[ "$PROJECT_TYPE" == "monorepo" ]]; then + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + packages_json="$(jq -c '[(.release.packages // [])[] | ascii_downcase + | gsub("[^a-z0-9]+"; "-") | gsub("^-+|-+$"; "")]' "$descriptor")" + if [[ "$(jq -r 'length' <<< "$packages_json")" -eq 0 ]]; then + echo "::error title=No release packages::A monorepo must declare release.packages in .github/inditextech-ci-java.json." >&2 + exit 1 + fi + fi + + git -C "$GITHUB_WORKSPACE" add -- "$WORKING_DIRECTORY/CHANGELOG.md" + if ! git -C "$GITHUB_WORKSPACE" diff --cached --quiet; then + git -C "$GITHUB_WORKSPACE" commit -m "[java-release] Consume changelog for release $RELEASE_VERSION" + fi + + baseline_before="$(git -C "$GITHUB_WORKSPACE" ls-remote origin "refs/heads/$BASELINE_BRANCH" | cut -f1)" + git -C "$GITHUB_WORKSPACE" ls-remote --tags origin \ + | awk '{print $2}' | sort > "$RUNNER_TEMP/origin-tags-before.txt" + + real_git="$(command -v git)" + interposer_dir="$(mktemp -d "$RUNNER_TEMP/ci-governance-git-interposer.XXXXXX")" + interposer_log="$RUNNER_TEMP/ci-governance-git-interposer-blocked.log" + : > "$interposer_log" + cat > "$interposer_dir/git" <<'GIT_SHIM' + #!/usr/bin/env bash + set -euo pipefail + subcommand="" + skip_next="false" + for arg in "$@"; do + if [[ "$skip_next" == "true" ]]; then + skip_next="false" + continue + fi + case "$arg" in + -C|-c|--git-dir|--work-tree|--namespace|--super-prefix|--exec-path|--config-env) + skip_next="true" + ;; + --git-dir=*|--work-tree=*|--namespace=*|--super-prefix=*|--exec-path=*|--config-env=*) + ;; + -*) + ;; + *) + subcommand="$arg" + break + ;; + esac + done + if [[ "$subcommand" == "push" ]]; then + { + printf 'blocked: git' + for arg in "$@"; do + printf ' %q' "$arg" + done + printf '\n' + } >> "$CI_GOVERNANCE_GIT_INTERPOSER_LOG" + exit 0 + fi + exec "$CI_GOVERNANCE_REAL_GIT" "$@" + GIT_SHIM + chmod +x "$interposer_dir/git" + + CI_GOVERNANCE_REAL_GIT="$real_git" \ + CI_GOVERNANCE_GIT_INTERPOSER_LOG="$interposer_log" \ + PATH="$interposer_dir:$PATH" \ + mvn -B --no-transfer-progress -f pom.xml \ + release:clean release:prepare \ + -DpushChanges=false \ + -DdevelopmentVersion="$NEXT_DEV" \ + -Darguments="-DskipTests" + + if [[ -s "$interposer_log" ]]; then + while IFS= read -r blocked_line; do + echo "::notice title=Blocked plugin push::$blocked_line" + done < "$interposer_log" + fi + + baseline_after="$(git -C "$GITHUB_WORKSPACE" ls-remote origin "refs/heads/$BASELINE_BRANCH" | cut -f1)" + git -C "$GITHUB_WORKSPACE" ls-remote --tags origin \ + | awk '{print $2}' | sort > "$RUNNER_TEMP/origin-tags-after.txt" + leaked_refs=() + while IFS= read -r ref; do + [[ -n "$ref" ]] && leaked_refs+=("$ref") + done < <(comm -13 "$RUNNER_TEMP/origin-tags-before.txt" "$RUNNER_TEMP/origin-tags-after.txt") + + if [[ "$baseline_after" != "$baseline_before" || "${#leaked_refs[@]}" -ne 0 ]]; then + for ref in "${leaked_refs[@]}"; do + git -C "$GITHUB_WORKSPACE" push origin --delete "$ref" \ + || echo "::warning title=Tag rollback failed::Could not delete $ref from origin." + done + if [[ "$baseline_after" != "$baseline_before" ]]; then + git -C "$GITHUB_WORKSPACE" push \ + --force-with-lease="refs/heads/$BASELINE_BRANCH:$baseline_after" \ + origin "$baseline_before:refs/heads/$BASELINE_BRANCH" \ + || echo "::warning title=Baseline rollback failed::$BASELINE_BRANCH still carries commits release:prepare pushed; reset it to $baseline_before by hand." + fi + echo "::error title=Release published before it was built::maven-release-plugin wrote to origin during prepare-release despite the engine's git interposer. Treat this as an interposer bypass and escalate to the CI governance engine owners." >&2 + exit 1 + fi + + git -C "$GITHUB_WORKSPACE" tag --list | sort > "$RUNNER_TEMP/java-tags-after.txt" + new_tags=() + while IFS= read -r tag; do + [[ -n "$tag" ]] && new_tags+=("$tag") + done < <(comm -13 "$RUNNER_TEMP/java-tags-before.txt" "$RUNNER_TEMP/java-tags-after.txt") + if [[ "${#new_tags[@]}" -ne 1 ]]; then + echo "::error title=Ambiguous release tag::release:prepare must produce exactly one tag; found ${#new_tags[@]}." + exit 1 + fi + release_tag="${new_tags[0]}" + release_commit="$(git -C "$GITHUB_WORKSPACE" rev-list -n 1 "$release_tag")" + promote_commit="$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" + + git -C "$GITHUB_WORKSPACE" push --atomic --force origin \ + "HEAD:$CI_GOVERNANCE_RELEASE_STAGING_REF" \ + "refs/tags/$release_tag:$CI_GOVERNANCE_RELEASE_TAG_STAGING_REF" + git -C "$GITHUB_WORKSPACE" tag -d "$release_tag" + + releases="$(jq -cn \ + --argjson packages "$packages_json" \ + --arg version "$RELEASE_VERSION" \ + --arg tag "$release_tag" \ + --arg next_dev "$NEXT_DEV" \ + '[{packages: $packages, version: $version, tag: $tag, next_dev: $next_dev}]')" + { + echo "release_commit=$release_commit" + echo "promote_commit=$promote_commit" + echo "releases<> "$GITHUB_OUTPUT" + + - name: Publish release metadata + id: release-metadata + env: + LIFECYCLE_MODE: ${{ steps.lifecycle.outputs.mode }} + RELEASES: ${{ steps.release-plan.outputs.releases_intermediate }} + DELEGATED_RELEASES: ${{ steps.delegated-release.outputs.releases }} + DELEGATED_RELEASE_COMMIT: ${{ steps.delegated-release.outputs.release_commit }} + DELEGATED_PROMOTE_COMMIT: ${{ steps.delegated-release.outputs.promote_commit }} + BASELINE_BRANCH: ${{ steps.release-context.outputs.baseline_branch }} + PLAN_EXPECTED_REF: ${{ steps.release-plan.outputs.expected_ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + if [[ "$LIFECYCLE_MODE" == "delegated" ]]; then + release_commit="$DELEGATED_RELEASE_COMMIT" + release_records="$DELEGATED_RELEASES" + promote_commit="$DELEGATED_PROMOTE_COMMIT" + expected_ref="refs/heads/$BASELINE_BRANCH" + staged_tag_ref="$CI_GOVERNANCE_RELEASE_TAG_STAGING_REF" + else + release_commit="$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" + release_records="$RELEASES" + promote_commit="$release_commit" + expected_ref="$PLAN_EXPECTED_REF" + staged_tag_ref="" + fi + { + echo "release_commit=$release_commit" + echo "promote_commit=$promote_commit" + echo "expected_ref=$expected_ref" + echo "staging_ref=$CI_GOVERNANCE_RELEASE_STAGING_REF" + echo "staged_tag_ref=$staged_tag_ref" + echo "releases<> "$GITHUB_OUTPUT" + + - name: Create release source handoff + id: release-source-handoff + env: + SOURCE_COMMIT: ${{ steps.release-metadata.outputs.release_commit }} + shell: bash + run: | + set -euo pipefail + if [[ ! "$SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]] \ + || ! git -C "$GITHUB_WORKSPACE" cat-file -e "$SOURCE_COMMIT^{commit}"; then + echo "::error title=Invalid release source::Release metadata did not provide a local immutable commit." >&2 + exit 1 + fi + handoff_dir="$RUNNER_TEMP/release-source" + bundle="$handoff_dir/release.bundle" + metadata="$handoff_dir/release-metadata.json" + artifact_name="java-release-source-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + mkdir -p "$handoff_dir" + git -C "$GITHUB_WORKSPACE" bundle create --version=2 "$bundle" --all "$SOURCE_COMMIT" + git -C "$GITHUB_WORKSPACE" bundle verify "$bundle" + bundle_sha256="$(sha256sum "$bundle" | awk '{print $1}')" + jq -n \ + --arg run_id "$GITHUB_RUN_ID" \ + --arg run_attempt "$GITHUB_RUN_ATTEMPT" \ + --arg source_commit "$SOURCE_COMMIT" \ + --arg bundle_sha256 "$bundle_sha256" \ + --arg artifact_name "$artifact_name" \ + '{run_id: $run_id, run_attempt: $run_attempt, source_commit: $source_commit, bundle_sha256: $bundle_sha256, bundle_format: "git-bundle-v2", artifact_name: $artifact_name}' \ + > "$metadata" + { + echo "artifact_name=$artifact_name" + echo "bundle_sha256=$bundle_sha256" + } >> "$GITHUB_OUTPUT" + + - name: Upload release source handoff + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ steps.release-source-handoff.outputs.artifact_name }} + path: | + ${{ runner.temp }}/release-source/release.bundle + ${{ runner.temp }}/release-source/release-metadata.json + if-no-files-found: error + retention-days: 14 + + build-distributions: + name: Build ${{ matrix.release.tag }} + needs: prepare-release + runs-on: ubuntu-24.04 + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + release: ${{ fromJSON(needs.prepare-release.outputs.releases) }} + steps: + - name: Download release source handoff + env: + EXPECTED_ARTIFACT: ${{ needs.prepare-release.outputs.release_source_artifact }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + gh run download "$GITHUB_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name "$EXPECTED_ARTIFACT" \ + --dir "$RUNNER_TEMP/release-source" + + - name: Verify and materialize release source + env: + EXPECTED_RUN_ID: ${{ github.run_id }} + EXPECTED_RUN_ATTEMPT: ${{ github.run_attempt }} + EXPECTED_SOURCE_COMMIT: ${{ needs.prepare-release.outputs.release_commit }} + EXPECTED_BUNDLE_SHA256: ${{ needs.prepare-release.outputs.release_source_sha256 }} + EXPECTED_ARTIFACT: ${{ needs.prepare-release.outputs.release_source_artifact }} + shell: bash + run: | + set -euo pipefail + bundle="$RUNNER_TEMP/release-source/release.bundle" + metadata="$RUNNER_TEMP/release-source/release-metadata.json" + if [[ ! -f "$bundle" || ! -f "$metadata" ]] \ + || [[ ! "$EXPECTED_SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]] \ + || [[ ! "$EXPECTED_BUNDLE_SHA256" =~ ^[0-9a-f]{64}$ ]]; then + echo "::error title=Invalid release source handoff::The release bundle or its immutable metadata is missing." >&2 + exit 1 + fi + if ! jq -e \ + --arg run_id "$EXPECTED_RUN_ID" \ + --arg run_attempt "$EXPECTED_RUN_ATTEMPT" \ + --arg source_commit "$EXPECTED_SOURCE_COMMIT" \ + --arg bundle_sha256 "$EXPECTED_BUNDLE_SHA256" \ + --arg artifact_name "$EXPECTED_ARTIFACT" \ + 'type == "object" and .run_id == $run_id and .run_attempt == $run_attempt and .source_commit == $source_commit and .bundle_sha256 == $bundle_sha256 and .bundle_format == "git-bundle-v2" and .artifact_name == $artifact_name' \ + "$metadata" > /dev/null; then + echo "::error title=Invalid release source handoff::Release source metadata does not match this run." >&2 + exit 1 + fi + actual_bundle_sha256="$(sha256sum "$bundle" | awk '{print $1}')" + if [[ "$actual_bundle_sha256" != "$EXPECTED_BUNDLE_SHA256" ]]; then + echo "::error title=Release source digest mismatch::The downloaded release bundle does not match its prepared digest." >&2 + exit 1 + fi + verifier="$RUNNER_TEMP/release-source-verifier" + git init --quiet "$verifier" + git -C "$verifier" bundle verify "$bundle" + if find "$GITHUB_WORKSPACE" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then + echo "::error title=Unexpected release workspace::Build distributions requires an empty workspace before materializing the handoff." >&2 + exit 1 + fi + git clone --no-checkout "$bundle" "$GITHUB_WORKSPACE" + git -C "$GITHUB_WORKSPACE" checkout --detach "$EXPECTED_SOURCE_COMMIT" + if [[ "$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" != "$EXPECTED_SOURCE_COMMIT" ]]; then + echo "::error title=Release source commit mismatch::The materialized source is not the prepared release commit." >&2 + exit 1 + fi + + - name: Resolve governed project type + shell: bash + run: | + set -euo pipefail + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + if [[ ! -f "$descriptor" ]]; then + echo "::error title=Missing descriptor::.github/inditextech-ci-java.json not found." + exit 1 + fi + project_type="$(jq -r '.project_type // empty' "$descriptor")" + case "$project_type" in + single|monorepo) ;; + *) + echo "::error title=Invalid project type::Descriptor .project_type must be single or monorepo." + exit 1 + ;; + esac + case "${WORKING_DIRECTORY:-}" in + ""|/*|..|../*|*/..|*/../*) + echo "::error title=Invalid working directory::WORKING_DIRECTORY must be a non-empty relative repository path." >&2 + exit 1 + ;; + esac + if [[ ! -d "$WORKING_DIRECTORY" ]]; then + echo "::error title=Missing working directory::'${WORKING_DIRECTORY}' is not a directory in this checkout." >&2 + exit 1 + fi + echo "PROJECT_TYPE=$project_type" >> "$GITHUB_ENV" + + - name: Set up workflow Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '25.0.2+10.0.LTS' + cache: maven + cache-dependency-path: ${{ env.WORKING_DIRECTORY }}/**/pom.xml + + - 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 Java + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.0 + with: + tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} + asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + + - name: Resolve build JDK from .tool-versions + id: jdk + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + echo "java_home=$(asdf where java)" >> "$GITHUB_OUTPUT" + + - name: Build declared artifacts + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + RELEASE: ${{ toJSON(matrix.release) }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + dist="$RUNNER_TEMP/dist" + rm -rf "$dist" + mkdir -p "$dist" + mvn -B --no-transfer-progress -f pom.xml -DskipTests -DskipEnforceSnapshots clean package + + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + declared="$(jq -r '.release.artifacts // [] | .[]' "$descriptor")" + artifact_ids="$(jq -c '.packages' <<< "$RELEASE")" + if [[ "$PROJECT_TYPE" == "monorepo" ]] \ + && [[ "$(jq -r 'length' <<< "$artifact_ids")" -eq 0 ]] \ + && [[ "$(jq -r '.release.strategy // empty' "$descriptor")" == "locked-step" ]]; then + artifact_ids="$(jq -c '[(.release.packages // [])[] | ascii_downcase + | gsub("[^a-z0-9]+"; "-") | gsub("^-+|-+$"; "")]' "$descriptor")" + fi + + is_release_distribution() { + case "$(basename "$1")" in + *-tests.jar|*-sources.jar|*-javadoc.jar) return 1 ;; + esac + [[ -f "$1" && "$1" == *.jar ]] + } + + if [[ -n "$declared" ]]; then + shopt -s nullglob + root="$PWD" + while IFS= read -r pattern; do + [[ -n "$pattern" ]] || continue + case "$pattern" in + /*|*'**'*|*..*) + echo "::error title=Invalid release artifact::release.artifacts entry '$pattern' must be a relative path inside the working directory, without '..' or '**'." + exit 1 + ;; + esac + saved_ifs="$IFS" + IFS= + # shellcheck disable=SC2206 + matches=($pattern) + IFS="$saved_ifs" + selected=0 + for match in ${matches[@]+"${matches[@]}"}; do + [[ -f "$match" && "$match" == *.jar ]] || continue + resolved="$(cd "$(dirname "$match")" && pwd -P)/$(basename "$match")" + case "$resolved" in + "$root"/*) ;; + *) + echo "::error title=Invalid release artifact::release.artifacts entry '$pattern' resolved outside the working directory." + exit 1 + ;; + esac + cp "$resolved" "$dist"/ + selected=$((selected + 1)) + done + if [[ "$selected" -eq 0 ]]; then + echo "::error title=Unmatched release artifact::release.artifacts entry '$pattern' matched no jar." + exit 1 + fi + done <<< "$declared" + elif [[ "$(jq -r 'length' <<< "$artifact_ids")" -gt 0 ]]; then + release_version="$(jq -r '.version // .tag' <<< "$RELEASE")" + while IFS= read -r module; do + [[ -n "$module" ]] || continue + selected=0 + # Select the module JAR by Maven artifactId at arbitrary depth. + while IFS= read -r jar; do + is_release_distribution "$jar" || continue + cp "$jar" "$dist"/ + selected=$((selected + 1)) + done < <(find . -type f \ + -path "*/target/${module}-${release_version}.jar") + if [[ "$selected" -eq 0 ]]; then + echo "::error title=Undeclared release artifacts::Module '$module' is published but produced no ${module}-${release_version}.jar, so the assets to attach cannot be inferred. Declare release.artifacts in .github/inditextech-ci-java.json." + exit 1 + fi + done < <(jq -r '.[]' <<< "$artifact_ids") + elif [[ "$PROJECT_TYPE" == "single" ]]; then + cp target/*.jar "$dist"/ 2>/dev/null || true + if ! ls "$dist"/*.jar > /dev/null 2>&1; then + while IFS= read -r jar; do + is_release_distribution "$jar" || continue + cp "$jar" "$dist"/ 2>/dev/null || true + done < <(find . -type f -name '*.jar' -path '*/target/*' ! -path '*/target/*/*') + fi + else + echo "::error title=Undeclared release artifacts::This reactor publishes no package list, so the jars to attach cannot be inferred. Declare release.artifacts in .github/inditextech-ci-java.json." + exit 1 + fi + + if ! ls "$dist"/*.jar > /dev/null 2>&1; then + echo "::error title=Missing artifacts::No jars were produced for ${RELEASE}." + exit 1 + fi + + - name: Upload release artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: java-distributions-${{ matrix.release.tag }} + path: ${{ runner.temp }}/dist/* + if-no-files-found: error + retention-days: 14 + + finalize-release: + name: Finalize release (promote + next dev) + needs: [prepare-release, build-distributions] + if: "!cancelled() && needs.prepare-release.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: Resolve governed project type + shell: bash + run: | + set -euo pipefail + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + if [[ ! -f "$descriptor" ]]; then + echo "::error title=Missing descriptor::.github/inditextech-ci-java.json not found." + exit 1 + fi + project_type="$(jq -r '.project_type // empty' "$descriptor")" + case "$project_type" in + single|monorepo) ;; + *) + echo "::error title=Invalid project type::Descriptor .project_type must be single or monorepo." + exit 1 + ;; + esac + case "${WORKING_DIRECTORY:-}" in + ""|/*|..|../*|*/..|*/../*) + echo "::error title=Invalid working directory::WORKING_DIRECTORY must be a non-empty relative repository path." >&2 + exit 1 + ;; + esac + if [[ ! -d "$WORKING_DIRECTORY" ]]; then + echo "::error title=Missing working directory::'${WORKING_DIRECTORY}' is not a directory in this checkout." >&2 + exit 1 + fi + echo "PROJECT_TYPE=$project_type" >> "$GITHUB_ENV" + + - name: Set up workflow Java + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '25.0.2+10.0.LTS' + cache: maven + cache-dependency-path: ${{ env.WORKING_DIRECTORY }}/**/pom.xml + + - 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 Java + uses: asdf-vm/actions/install@1902764435ca0dd2f3388eea723a4f92a4eb8302 # v4.0.0 + with: + tool_versions: ${{ steps.tool-versions.outputs.tool_versions }} + asdf_version: ${{ env.ASDF_BRANCH_VERSION }} + + - name: Resolve build JDK from .tool-versions + id: jdk + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + echo "java_home=$(asdf where java)" >> "$GITHUB_OUTPUT" + + - name: Promote the release to the default branch + id: promote + if: 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 }} + PROMOTE_COMMIT: ${{ needs.prepare-release.outputs.promote_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)" == "$PROMOTE_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)" != "$PROMOTE_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" "$RELEASE_COMMIT" + tag_refs+=("refs/tags/$tag") + done < <(jq -c '.[]' <<< "$RELEASES") + + git push --atomic origin "${PROMOTE_COMMIT}:${EXPECTED_REF}" "${tag_refs[@]}" + + - name: Next development iteration and sync PR + if: steps.promote.outcome == 'success' + env: + JAVA_HOME: ${{ steps.jdk.outputs.java_home }} + 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.prepare-release.outputs.releases }} + RELEASE_COMMIT: ${{ needs.prepare-release.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" + + descriptor="$GITHUB_WORKSPACE/.github/inditextech-ci-java.json" + release_mode="$(jq -r '.release.lifecycle // "govern-inject"' "$descriptor")" + + if [[ "$release_mode" == "delegated" ]]; then + echo "::notice title=Next-dev skipped::Delegated release already advanced next development." + elif [[ "$(git rev-parse HEAD)" != "$RELEASE_COMMIT" ]]; then + echo "::notice title=Next-dev skipped::Branch already advanced past the release commit." + else + if [[ "$PROJECT_TYPE" == "single" ]]; then + next_dev="$(jq -r '.[0].next_dev' <<< "$RELEASES")" + mvn -q -B --no-transfer-progress -f pom.xml versions:set \ + -DnewVersion="$next_dev" -DgenerateBackupPoms=false + elif [[ "$(jq -er '.release.strategy' "$descriptor")" == "locked-step" ]]; then + next_dev="$(jq -r '.[0].next_dev' <<< "$RELEASES")" + mvn -q -B --no-transfer-progress -f pom.xml versions:set \ + -DnewVersion="$next_dev" -DprocessAllModules=true -DgenerateBackupPoms=false + else + while IFS= read -r record; do + artifact="$(jq -r '.packages[0]' <<< "$record")" + next_dev="$(jq -r '.next_dev' <<< "$record")" + mvn -q -B --no-transfer-progress -f pom.xml versions:set \ + -DgroupId=com.inditextech -DartifactId="$artifact" -DoldVersion='*' \ + -DnewVersion="$next_dev" -DprocessAllModules=false -DgenerateBackupPoms=false + done < <(jq -c '.[]' <<< "$RELEASES") + fi + git -C "$GITHUB_WORKSPACE" add --update -- "$WORKING_DIRECTORY" + git commit -m "[java-release] prepare for next development iteration" + 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 + + - name: Discard the release staging ref + if: always() + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + STAGING_REF: ${{ needs.prepare-release.outputs.staging_ref }} + STAGED_TAG_REF: ${{ needs.prepare-release.outputs.staged_tag_ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${STAGING_REF:-}" ]]; then + exit 0 + fi + gh auth setup-git + refs=(":${STAGING_REF}") + if [[ -n "${STAGED_TAG_REF:-}" ]]; then + refs+=(":${STAGED_TAG_REF}") + fi + git push origin "${refs[@]}" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..7f226a40 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: CodeQL + +on: + push: + branches: [main, main-*, develop, develop-*] + pull_request: + branches: [main, main-*, develop, develop-*] + merge_group: + types: [checks_requested] + schedule: + - cron: '17 4 * * 1' + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + scanning-availability: + name: Resolve code scanning availability + if: >- + github.event_name == 'schedule' || + ( + vars.DEVELOPMENT_FLOW == 'trunk-based-development' && + ( + (github.event_name == 'push' && (github.ref_name == 'main' || startsWith(github.ref_name, 'main-'))) || + (github.event_name == 'pull_request' && (github.event.pull_request.base.ref == 'main' || startsWith(github.event.pull_request.base.ref, 'main-'))) || + (github.event_name == 'merge_group' && (github.event.merge_group.base_ref == 'main' || startsWith(github.event.merge_group.base_ref, 'main-'))) + ) + ) || + ( + vars.DEVELOPMENT_FLOW == 'git-flow' && + ( + (github.event_name == 'push' && (github.ref_name == 'develop' || startsWith(github.ref_name, 'develop-'))) || + (github.event_name == 'pull_request' && (github.event.pull_request.base.ref == 'develop' || startsWith(github.event.pull_request.base.ref, 'develop-'))) || + (github.event_name == 'merge_group' && (github.event.merge_group.base_ref == 'develop' || startsWith(github.event.merge_group.base_ref, 'develop-'))) + ) + ) + runs-on: ubuntu-24.04 + permissions: + contents: read + outputs: + enabled: ${{ steps.resolve.outputs.enabled }} + steps: + - name: Resolve availability + id: resolve + 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" ]; then + enabled=true + elif [ "${seat}" = "enabled" ]; then + enabled=true + elif [ "${FORCE_ENABLED:-}" = "true" ]; then + enabled=true + fi + echo "Code scanning availability: visibility=${visibility} seat=${seat} enabled=${enabled}" + echo "enabled=${enabled}" >> "${GITHUB_OUTPUT}" + + analyze: + name: Analyze (${{ matrix.language }}) + needs: scanning-availability + if: needs.scanning-availability.outputs.enabled == 'true' + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + packages: read + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: java + build-mode: none + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4 + with: + build-mode: ${{ matrix.build-mode }} + languages: ${{ matrix.language }} + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4 + with: + category: /language:${{ matrix.language }} diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml new file mode 100644 index 00000000..66c196d0 --- /dev/null +++ b/.github/workflows/pr-verify.yml @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: Pull Request Verification +permissions: + contents: read + +on: + pull_request: + +concurrency: + group: pr-verify-${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + workflow-quality: + name: Workflow and Template Quality + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + checks: write # Lets reviewdog publish actionlint check results. + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: Lint GitHub Actions workflows + uses: reviewdog/action-actionlint@dbe5299849118fd6f099ba563d263d770955a64a # v1.73.2 + with: + reporter: github-pr-check + fail_level: error + + repo-linter: + name: Repo Linter + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: 'Run Repolinter' + uses: newrelic/repolinter-action@3f4448f855c351e9695b24524a4111c7847b84cb # v1.7.0 + + reuse-compliance: + name: REUSE Compliance + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: REUSE Compliance Check + uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 + + conventional-commits: + name: Conventional Commits + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - uses: webiny/action-conventional-commits@7f91b1595ca1951cdb671ddc9f07a49081ec5b69 # v1.4.2 diff --git a/.github/workflows/push-verify.yml b/.github/workflows/push-verify.yml new file mode 100644 index 00000000..b5c40f9f --- /dev/null +++ b/.github/workflows/push-verify.yml @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: Push Verification +permissions: + contents: read + +on: + push: + +concurrency: + group: push-verify-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + workflow-quality: + if: >- + github.ref_type == 'branch' && + github.ref_name == github.event.repository.default_branch + name: Workflow and Template Quality + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + checks: write # Lets reviewdog publish actionlint check results. + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Lint GitHub Actions workflows + uses: reviewdog/action-actionlint@dbe5299849118fd6f099ba563d263d770955a64a # v1.73.2 + with: + reporter: github-check + fail_level: error + + repo-linter: + if: >- + github.ref_type == 'branch' && + github.ref_name == github.event.repository.default_branch + name: Repo Linter + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: 'Run Repolinter' + uses: newrelic/repolinter-action@3f4448f855c351e9695b24524a4111c7847b84cb # v1.7.0 + + reuse-compliance: + if: >- + github.ref_type == 'branch' && + github.ref_name == github.event.repository.default_branch + name: REUSE Compliance + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: REUSE Compliance Check + uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml new file mode 100644 index 00000000..7ee05235 --- /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 new file mode 100644 index 00000000..de7c21af --- /dev/null +++ b/.github/workflows/sync-to-develop.yml @@ -0,0 +1,204 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: Sync default branch to develop + +on: + # The workflow checks out only the merged base branch, never fork-provided code. + pull_request_target: + types: [closed] + +permissions: + contents: read + +jobs: + sync-to-develop: + name: Synchronize default branch to develop + 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 + concurrency: + 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 + with: + client-id: ${{ vars.APP_CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + 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: Prepare sync branch + id: prepare + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + BASE_BRANCH: ${{ github.event.pull_request.base.ref }} + DEFAULT_BRANCH: ${{ github.event.repository.default_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 + + sync_branch="automated/sync-from-${BASE_BRANCH}-to-${develop_branch}" + + gh auth setup-git + git config user.name "${APP_SLUG}[bot]" + git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + + if ! git ls-remote --exit-code --heads origin "$develop_branch" > /dev/null; then + echo "::notice title=Sync skipped::The $develop_branch branch does not exist." + echo "should_sync=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git fetch --no-tags origin "$BASE_BRANCH" "$develop_branch" + + if git merge-base --is-ancestor "origin/$BASE_BRANCH" "origin/$develop_branch"; then + echo "::notice title=Sync skipped::$develop_branch already contains $BASE_BRANCH." + echo "should_sync=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if git ls-remote --exit-code --heads origin "$sync_branch" > /dev/null; then + git fetch --no-tags origin "$sync_branch" + git switch --force-create "$sync_branch" "origin/$sync_branch" + # Preserve merge commits created by conflict resolution on the sync pull request. + git rebase --rebase-merges "origin/$BASE_BRANCH" + git push \ + --force-with-lease="refs/heads/$sync_branch:$(git rev-parse "origin/$sync_branch")" \ + origin \ + "HEAD:refs/heads/$sync_branch" + else + git switch --create "$sync_branch" "origin/$BASE_BRANCH" + git push --set-upstream origin "$sync_branch" + fi + + { + echo "develop_branch=$develop_branch" + echo "should_sync=true" + echo "sync_branch=$sync_branch" + } >> "$GITHUB_OUTPUT" + + - name: Create or update sync pull request + if: steps.prepare.outputs.should_sync == 'true' + env: + BASE_BRANCH: ${{ github.event.pull_request.base.ref }} + DEVELOP_BRANCH: ${{ steps.prepare.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 }} + run: | + set -euo pipefail + + body="$(printf '%s\n\n- #%s' \ + '**Automated pull request** related to:' \ + "$SOURCE_PULL_REQUEST")" + existing_pr="$(gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --head "$SYNC_BRANCH" \ + --base "$DEVELOP_BRANCH" \ + --state open \ + --json isCrossRepository,number \ + --jq '[.[] | select(.isCrossRepository == false)][0].number // empty')" + has_internal_label="$(gh label list \ + --repo "$GITHUB_REPOSITORY" \ + --search kind/internal \ + --json name \ + --jq 'any(.[]; .name == "kind/internal")')" + + if [[ -n "$existing_pr" ]]; then + gh pr edit \ + "$existing_pr" \ + --repo "$GITHUB_REPOSITORY" \ + --body "$body" + + if [[ "$has_internal_label" == "true" ]]; then + gh pr edit \ + "$existing_pr" \ + --repo "$GITHUB_REPOSITORY" \ + --add-label kind/internal + else + echo "::warning title=Missing label::The kind/internal label is not available." + fi + else + if [[ "$has_internal_label" == "true" ]]; then + gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --base "$DEVELOP_BRANCH" \ + --head "$SYNC_BRANCH" \ + --title "Sync from $BASE_BRANCH to $DEVELOP_BRANCH" \ + --label kind/internal \ + --body "$body" + else + echo "::warning title=Missing label::The kind/internal label is not available." + gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --base "$DEVELOP_BRANCH" \ + --head "$SYNC_BRANCH" \ + --title "Sync from $BASE_BRANCH to $DEVELOP_BRANCH" \ + --body "$body" + fi + fi + + - name: Comment on source pull request after failure + if: >- + failure() && + !cancelled() && + steps.app-token.outcome == 'success' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + SOURCE_PULL_REQUEST: ${{ github.event.pull_request.number }} + run: | + gh pr comment \ + "$SOURCE_PULL_REQUEST" \ + --repo "$GITHUB_REPOSITORY" \ + --body "The automatic synchronization to develop failed. See the [workflow log](https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)." diff --git a/.sync-exclude b/.sync-exclude new file mode 100644 index 00000000..8329f1cd --- /dev/null +++ b/.sync-exclude @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +# One normalized relative path per line. Lines in this file are not reconciled by InditexTech CI governance. +.github/PULL_REQUEST_TEMPLATE.md +.github/workflows/code-maven_java-PR_verify.yml +.github/workflows/code-maven_java-release.yml +.github/workflows/code-maven_java-sonarcloud-analysis.yml +.github/workflows/code-release_preview.yml +CODE_OF_CONDUCT.md +CONTRIBUTING.md +SECURITY.md +code/.tool-versions diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..6060d1c4 --- /dev/null +++ b/NOTICE @@ -0,0 +1,3 @@ +Copyright 2024 INDITEX S.A. + +SPDX-License-Identifier: Apache-2.0 diff --git a/REUSE.toml b/REUSE.toml index 79b344c1..61e5d2fa 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -43,6 +43,18 @@ SPDX-FileCopyrightText = "2024 INDITEX S.A" SPDX-License-Identifier = "Apache-2.0" SPDX-FileComment = "Karate tools workflows" +[[annotations]] +path = ".github/inditextech-ci-java.json" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 INDITEX S.A" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".github/inditextech-ci-sync-manifest.json" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 INDITEX S.A" +SPDX-License-Identifier = "Apache-2.0" + [[annotations]] path = ".gitignore" precedence = "aggregate" @@ -168,6 +180,12 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2024 INDITEX S.A" SPDX-License-Identifier = "Apache-2.0" +[[annotations]] +path = "repolinter.json" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 INDITEX S.A" +SPDX-License-Identifier = "Apache-2.0" + [[annotations]] path = "REUSE.toml" precedence = "aggregate" diff --git a/repolinter.json b/repolinter.json new file mode 100644 index 00000000..4e64643e --- /dev/null +++ b/repolinter.json @@ -0,0 +1,258 @@ +{ + "$schema": "https://raw.githubusercontent.com/todogroup/repolinter/master/rulesets/schema.json", + "version": 2, + "axioms": { + "linguist": "language", + "licensee": "license", + "packagers": "packager" + }, + "rules": { + "license-file-exists": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["LICENSE*", "COPYING*"], + "nocase": true + } + } + }, + "readme-file-exists": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["README*"], + "nocase": true + } + } + }, + "contributing-file-exists": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["{docs/,.github/,}CONTRIB*"], + "nocase": true + } + } + }, + "code-of-conduct-file-exists": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "globsAny": [ + "{docs/,.github/,}CODEOFCONDUCT*", + "{docs/,.github/,}CODE-OF-CONDUCT*", + "{docs/,.github/,}CODE_OF_CONDUCT*" + ], + "nocase": true + } + } + }, + "security-file-exists": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["{docs/,.github/,}SECURITY.md"] + } + } + }, + "readme-references-license": { + "level": "error", + "rule": { + "type": "file-contents", + "options": { + "globsAll": ["README*"], + "content": "license", + "flags": "i" + } + } + }, + "binaries-not-present": { + "level": "error", + "rule": { + "type": "file-type-exclusion", + "options": { + "type": ["/*.exe", "/.dll", "!node_modules/"] + } + } + }, + "integrates-with-ci": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "globsAny": [ + ".github/workflows/" + ] + } + } + }, + "code-of-conduct-file-contains-email": { + "level": "error", + "rule": { + "type": "file-contents", + "options": { + "globsAll": [ + "CODEOFCONDUCT", + "CODE-OF-CONDUCT*", + "CODE_OF_CONDUCT*", + ".github/CODEOFCONDUCT*", + ".github/CODE-OF-CONDUCT*", + ".github/CODE_OF_CONDUCT*" + ], + "content": ".+@.+..+", + "flags": "i", + "human-readable-content": "email address" + } + } + }, + "github-issue-template-exists": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "dirs": true, + "globsAny": ["ISSUE_TEMPLATE", ".github/ISSUE_TEMPLATE*"] + } + } + }, + "github-pull-request-template-exists": { + "level": "error", + "rule": { + "type": "file-existence", + "options": { + "dirs": true, + "globsAny": [ + "PULL_REQUEST_TEMPLATE*", + ".github/PULL_REQUEST_TEMPLATE*" + ] + } + } + }, + "javascript-package-metadata-exists": { + "level": "error", + "where": ["language=javascript"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["{,**/}package.json"] + } + } + }, + "ruby-package-metadata-exists": { + "level": "error", + "where": ["language=ruby"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["Gemfile"] + } + } + }, + "java-package-metadata-exists": { + "level": "error", + "where": ["language=java"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["{,**/}pom.xml", "{,**/}build.xml", "{,**/}build.gradle"] + } + } + }, + "python-package-metadata-exists": { + "level": "error", + "where": ["language=python"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": [ + "{,**/}setup.py", + "{,**/}requirements.txt", + "{,**/}pyproject.toml" + ] + } + } + }, + "objective-c-package-metadata-exists": { + "level": "error", + "where": ["language=objective-c"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["Cartfile", "Podfile", ".podspec"] + } + } + }, + "swift-package-metadata-exists": { + "level": "error", + "where": ["language=swift"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["Package.swift"] + } + } + }, + "erlang-package-metadata-exists": { + "level": "error", + "where": ["language=erlang"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["rebar.config"] + } + } + }, + "elixir-package-metadata-exists": { + "level": "error", + "where": ["language=elixir"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["mix.exs"] + } + } + }, + "license-detectable-by-licensee": { + "level": "error", + "where": ["license="], + "rule": { + "type": "license-detectable-by-licensee", + "options": {} + } + }, + "notice-file-exists": { + "level": "error", + "where": ["license=Apache-2.0"], + "rule": { + "type": "file-existence", + "options": { + "globsAny": ["NOTICE*"], + "fail-message": "The NOTICE file is described in section 4.4 of the Apache License version 2.0. Its presence is not mandated by the license itself, but by ASF policy." + } + } + }, + "internal-file-not-exists": { + "level": "error", + "rule": { + "type": "file-not-exists", + "options": { + "globsAll": [ + ".secrets.baseline", + "sherpa-config.yml", + ".snyk", + "sonar-project.properties", + ".drafterconfig.yml", + "application-configmap.yml", + "application-secret.yml" + ], + "nocase": true + } + } + } + } +} \ No newline at end of file