From eebaf5d2c7a93a9588fb3afbf8910e16cbb85c82 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:03:46 +0000 Subject: [PATCH 1/7] chore(ci-governance): adopt Go profile Governance-Provenance: hmac-sha256:16596c659e24320e4d934bc04df87ef67ec8961ec4c39cc29c2eedc38a9abf8e --- .github/ISSUE_TEMPLATE/1-bug.yml | 50 ++ .github/ISSUE_TEMPLATE/2-feature.yml | 33 + .github/ISSUE_TEMPLATE/3-other.yml | 37 + .github/ISSUE_TEMPLATE/config.yml | 3 + .github/PULL_REQUEST_TEMPLATE.md | 21 + .github/inditextech-ci-go.json | 7 + .github/inditextech-ci-sync-manifest.json | 54 ++ .github/workflows/code-gomod_go-PR_verify.yml | 107 +++ .../workflows/code-gomod_go-release-core.yml | 691 ++++++++++++++++++ .github/workflows/code-gomod_go-release.yml | 98 +++ .../code-gomod_go-sonarcloud-analysis.yml | 145 ++++ .github/workflows/code-release_preview.yml | 155 ++++ .github/workflows/codeql.yml | 155 ++++ .github/workflows/pr-verify.yml | 79 ++ .github/workflows/push-verify.yml | 68 ++ .github/workflows/scorecard-analysis.yml | 56 ++ .sync-exclude | 5 + CODE_OF_CONDUCT.md | 8 + SECURITY.md | 65 +- repolinter.json | 81 +- 20 files changed, 1827 insertions(+), 91 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/1-bug.yml create mode 100644 .github/ISSUE_TEMPLATE/2-feature.yml create mode 100644 .github/ISSUE_TEMPLATE/3-other.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/inditextech-ci-go.json create mode 100644 .github/inditextech-ci-sync-manifest.json create mode 100644 .github/workflows/code-gomod_go-PR_verify.yml create mode 100644 .github/workflows/code-gomod_go-release-core.yml create mode 100644 .github/workflows/code-gomod_go-release.yml create mode 100644 .github/workflows/code-gomod_go-sonarcloud-analysis.yml create mode 100644 .github/workflows/code-release_preview.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/pr-verify.yml create mode 100644 .github/workflows/push-verify.yml create mode 100644 .github/workflows/scorecard-analysis.yml create mode 100644 .sync-exclude diff --git a/.github/ISSUE_TEMPLATE/1-bug.yml b/.github/ISSUE_TEMPLATE/1-bug.yml new file mode 100644 index 0000000..b03cd59 --- /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 0000000..b44bd15 --- /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 0000000..6968199 --- /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 0000000..30e40d3 --- /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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..954cfa2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + + +## Summary + +Briefly describe the purpose of this PR and what changes it introduces. + +## Checklist + +- [ ] Commits are **signed** (`git commit -S`) +- [ ] Commit messages follow **Conventional Commits** +- [ ] Documentation has been updated (if needed) +- [ ] I have read and agree to the project’s [Code of Conduct](../CODE_OF_CONDUCT.md) +- [ ] I have signed the [Contributor License Agreement (CLA)](../CONTRIBUTING.md) + +## Additional context + +Add any screenshots, test output, or notes for reviewers here. diff --git a/.github/inditextech-ci-go.json b/.github/inditextech-ci-go.json new file mode 100644 index 0000000..5a7b263 --- /dev/null +++ b/.github/inditextech-ci-go.json @@ -0,0 +1,7 @@ +{ + "artifacts": "none", + "schema_version": 1, + "sonar": true, + "verify_command": "make verify", + "working_directory": "." +} diff --git a/.github/inditextech-ci-sync-manifest.json b/.github/inditextech-ci-sync-manifest.json new file mode 100644 index 0000000..f03db01 --- /dev/null +++ b/.github/inditextech-ci-sync-manifest.json @@ -0,0 +1,54 @@ +{ + "configuration": { + "development_flow": "trunk-based", + "outputs": [ + "gh-release" + ], + "parameters": { + "artifacts": "none", + "sonar": true, + "verify_command": "make verify", + "working_directory": "." + } + }, + "creation_year": 2026, + "integrity": { + "algorithm": "hmac-sha256", + "signature": "cca1eb8de78a2504aa7b045db5a83427c6d4377738720c37cb8e43620cf4cbc2" + }, + "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/PULL_REQUEST_TEMPLATE.md": "23a0b0ac79a9020ccac9c013582a01f86e2df2ae7f914673583b9a3368313041", + ".github/inditextech-ci-go.json": "b0e32b653ec535b83e803ec54b688df558593b2ad234cb3cfaf9b42f209a7933", + ".github/workflows/code-gomod_go-PR_verify.yml": "76aefd67d70c17d44252cb2854191c6530cfc176f49d3a481a65eac2f4989fb9", + ".github/workflows/code-gomod_go-release-core.yml": "5eb3b8c7b5d09f9ad1b5d844b58bd0de877de1f13cd9e3dc2da5d6af0b793b07", + ".github/workflows/code-gomod_go-release.yml": "d2d6babc6d37f7d85eb12051d7ab05d0731471d8fea77a437eb51660df08135b", + ".github/workflows/code-gomod_go-sonarcloud-analysis.yml": "fcb5c72a4ae59a3b429420b1e15eda5c4c0cb4a0aa89c3f4f99eb2e3198dcaca", + ".github/workflows/code-release_preview.yml": "5bb379d87f946c3e5cca829b3d3a3071188cc045dd6ebe514732c06662f91181", + ".github/workflows/codeql.yml": "cdd995b56f89f38fbe2cdfd2701e8e6b1d632710da4d9768e8afdf1225a9454b", + ".github/workflows/pr-verify.yml": "f52295fbfe81578c0a6459d8b384be569933ac78d32fff370932a8dec6943e46", + ".github/workflows/push-verify.yml": "fd700ba0ee23fdee9cd7ecf23ad386247f4b621b73cc1946f9ef0eda4ea07086", + ".github/workflows/scorecard-analysis.yml": "5bd6da647f708cced5d33411cc27aac7f7ee2be06ae541defea677d70fe80481", + "CODE_OF_CONDUCT.md": "ce1e7a8f68a7917d48c03f9f7aae5529367f73af0e959276e889d33ea1e8d4ab", + "SECURITY.md": "0ee7a3356bc3a1c7649e3b7a6a9012b0608011be2b1ff8d106ad02c8950bab25", + "repolinter.json": "0efb305c47a63f03c488a34906fc2d5471c84145c2ff553f2aacf3a41b92e1a1" + }, + "managed_variables": { + "DEVELOPMENT_FLOW": "trunk-based-development", + "WORKING_DIRECTORY": "." + }, + "profile": "go", + "repository_id": 938683565, + "retained_paths": { + "CONTRIBUTING.md": "368be3edf5cae6b3fa0b10bc8ab2395f2e37e2e71f0013800c5d9d61e80b06d4" + }, + "schema_version": 2, + "source_digests": { + "base": "3d2a77fa08c7469099ee502cf516bb74e40735fb5f9ea93e0850bb45c6297e53", + "go": "31367912deed08276e552542d3f117d526d874c4c30206f99c5266ae465aae8e" + } +} diff --git a/.github/workflows/code-gomod_go-PR_verify.yml b/.github/workflows/code-gomod_go-PR_verify.yml new file mode 100644 index 0000000..613f56e --- /dev/null +++ b/.github/workflows/code-gomod_go-PR_verify.yml @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: code-gomod-go-PR-verify + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + +permissions: + contents: read + +concurrency: + group: code-gomod-go-pr-verify-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +env: + WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} + COMPONENT_DESCRIPTOR: .github/inditextech-ci-go.json + ASDF_BRANCH_VERSION: '0.18.0' + +jobs: + verify: + name: Code / Verify + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Check out pull request head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: Read governed tool versions + id: tool-versions + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + resolved="" + if [[ -f .tool-versions ]]; then + if grep -vE '^[[:space:]]*(#|$)' .tool-versions | grep -qvE '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$'; then + echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." + exit 1 + fi + resolved="$(grep -E '^golang ' .tool-versions || true)" + fi + if [[ -z "$resolved" ]]; then + if [[ ! -f go.mod ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY} has no golang pin in .tool-versions and no go.mod to derive one from." + exit 1 + fi + version="$(sed -nE 's/^toolchain[[:space:]]+go([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + if [[ -z "$version" ]]; then + version="$(sed -nE 's/^go[[:space:]]+([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + fi + if [[ -z "$version" ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY}/go.mod declares no usable go version." + exit 1 + fi + if [[ "$version" != *.*.* ]]; then + version="${version}.0" + fi + resolved="golang ${version}" + echo "::notice title=Derived go toolchain::Using ${resolved} from ${WORKING_DIRECTORY}/go.mod." + fi + { + echo "tool_versions<> "$GITHUB_OUTPUT" + + - name: Set up asdf-managed Go + 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: Resolve governed verify command + id: descriptor + shell: bash + run: | + set -euo pipefail + if [[ ! -f "$COMPONENT_DESCRIPTOR" ]]; then + echo "::error title=Missing descriptor::${COMPONENT_DESCRIPTOR} not found." + exit 1 + fi + verify_command="$(jq -er '.verify_command // "make verify"' "$COMPONENT_DESCRIPTOR")" + if [[ -z "$verify_command" ]]; then + echo "::error title=Empty verify command::descriptor verify_command must not be blank." + exit 1 + fi + { + echo "verify_command<> "$GITHUB_OUTPUT" + + - name: Verify and test + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + env: + VERIFY_COMMAND: ${{ steps.descriptor.outputs.verify_command }} + run: | + set -euo pipefail + bash -c "$VERIFY_COMMAND" diff --git a/.github/workflows/code-gomod_go-release-core.yml b/.github/workflows/code-gomod_go-release-core.yml new file mode 100644 index 0000000..1c22d30 --- /dev/null +++ b/.github/workflows/code-gomod_go-release-core.yml @@ -0,0 +1,691 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: code-gomod-go-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 and versions. + value: ${{ jobs.prepare-release.outputs.releases }} + artifacts: + description: The declared artifact mode ('none' or 'binaries'). + value: ${{ jobs.prepare-release.outputs.artifacts }} + release_source_artifact: + description: Run-bound artifact containing the sealed release source. + value: ${{ jobs.prepare-release.outputs.release_source_artifact }} + release_source_sha256: + description: SHA-256 digest of the sealed release source bundle. + value: ${{ jobs.prepare-release.outputs.release_source_sha256 }} + 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 }} + WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} + COMPONENT_DESCRIPTOR: .github/inditextech-ci-go.json + 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 }} + releases: ${{ steps.release-metadata.outputs.releases }} + artifacts: ${{ steps.release-metadata.outputs.artifacts }} + expected_ref: ${{ steps.release-plan.outputs.expected_ref }} + staging_ref: ${{ steps.release-metadata.outputs.staging_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: Read governed tool versions + id: tool-versions + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + resolved="" + if [[ -f .tool-versions ]]; then + if grep -vE '^[[:space:]]*(#|$)' .tool-versions | grep -qvE '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$'; then + echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." + exit 1 + fi + resolved="$(grep -E '^golang ' .tool-versions || true)" + fi + if [[ -z "$resolved" ]]; then + if [[ ! -f go.mod ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY} has no golang pin in .tool-versions and no go.mod to derive one from." + exit 1 + fi + version="$(sed -nE 's/^toolchain[[:space:]]+go([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + if [[ -z "$version" ]]; then + version="$(sed -nE 's/^go[[:space:]]+([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + fi + if [[ -z "$version" ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY}/go.mod declares no usable go version." + exit 1 + fi + if [[ "$version" != *.*.* ]]; then + version="${version}.0" + fi + resolved="golang ${version}" + echo "::notice title=Derived go toolchain::Using ${resolved} from ${WORKING_DIRECTORY}/go.mod." + fi + { + echo "tool_versions<> "$GITHUB_OUTPUT" + + - name: Set up asdf-managed Go + 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 + verify_command="$(jq -er '.verify_command // "make verify"' "$GITHUB_WORKSPACE/$COMPONENT_DESCRIPTOR")" + bash -c "$verify_command" + + - name: Plan release metadata + id: release-plan + 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_REF: ${{ steps.release-context.outputs.release_ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + + gh auth setup-git + + records_file="$RUNNER_TEMP/go-release-records.jsonl" + 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" + 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")" != "[go-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 + + 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" + } + + semver_gt() { + local lmajor lminor lpatch rmajor rminor rpatch + IFS=. read -r lmajor lminor lpatch <<< "$1" + IFS=. read -r rmajor rminor rpatch <<< "$2" + (( lmajor != rmajor )) && return $(( lmajor > rmajor ? 0 : 1 )) + (( lminor != rminor )) && return $(( lminor > rminor ? 0 : 1 )) + (( lpatch > rpatch )) + } + + if [[ "$resume" == "false" ]]; then + 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="0.0.0" + while IFS= read -r candidate; do + stripped="${candidate#v}" + if [[ "$stripped" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && semver_gt "$stripped" "$current_version"; then + current_version="$stripped" + fi + done < <(git tag --list 'v[0-9]*' '[0-9]*') + + release_version="$(bump_semver "$current_version" "$release_bump")" + tag="v${release_version}" + + if git show-ref --tags --verify --quiet "refs/tags/$tag"; then + echo "::error title=Tag conflict::$tag already exists." + exit 1 + fi + + jq -cn --arg version "$release_version" --arg tag "$tag" \ + '{version: $version, tag: $tag}' > "$records_file" + releases="$(jq -cs . "$records_file")" + echo "RELEASE_VERSION=$release_version" >> "$GITHUB_ENV" + echo "RELEASE_BUMP=$release_bump" >> "$GITHUB_ENV" + else + release_version="$(grep -E -m1 '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' CHANGELOG.md | sed -E 's/^## \[([0-9]+\.[0-9]+\.[0-9]+)\].*/\1/')" + if [[ -z "$release_version" ]]; then + echo "::error title=Incomplete release resume::CHANGELOG.md has no released version heading." + exit 1 + fi + tag="v${release_version}" + releases="$(jq -cn --arg version "$release_version" --arg tag "$tag" '[{version: $version, tag: $tag}]')" + head_commit="$(git rev-parse HEAD)" + 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 + 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' + 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: "v" + + - name: Reconcile CHANGELOG version with release + if: steps.release-plan.outputs.resume != 'true' + 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/v)${cl_ver_re}(\.\.\.HEAD)@\1${RELEASE_VERSION}\2@" \ + -e "s@^\[${cl_ver_re}\]: (.*/)(compare/[^ ]*\.\.\.|releases/tag/)v${cl_ver_re}@[${RELEASE_VERSION}]: \1\2v${RELEASE_VERSION}@" \ + "$cl" > "$tmp" + cat "$tmp" > "$cl" + rm -f "$tmp" + fi + + - name: Commit and stage release + if: steps.release-plan.outputs.resume != 'true' + 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" + if git diff --cached --quiet; then + echo "::error title=Missing release metadata::The CHANGELOG bump produced no 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 "[go-release] Prepare release" + + git check-ref-format "refs/tags/$(jq -r '.[0].tag' <<< "$RELEASES")" + + git push --atomic --force origin "HEAD:$CI_GOVERNANCE_RELEASE_STAGING_REF" + + - name: Publish release metadata + id: release-metadata + env: + RELEASES: ${{ steps.release-plan.outputs.releases_intermediate }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + artifacts="$(jq -er '.artifacts // "none"' "$GITHUB_WORKSPACE/$COMPONENT_DESCRIPTOR")" + { + echo "release_commit=$(git -C "$GITHUB_WORKSPACE" rev-parse HEAD)" + echo "artifacts=$artifacts" + echo "staging_ref=$CI_GOVERNANCE_RELEASE_STAGING_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="go-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 + if: needs.prepare-release.outputs.artifacts == 'binaries' + 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 + 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 + verifier="$RUNNER_TEMP/release-source-verifier" + git init --quiet "$verifier" + git -C "$verifier" bundle verify "$bundle" + 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: Read governed tool versions + id: tool-versions + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + resolved="" + if [[ -f .tool-versions ]]; then + if grep -vE '^[[:space:]]*(#|$)' .tool-versions | grep -qvE '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$'; then + echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." + exit 1 + fi + resolved="$(grep -E '^golang ' .tool-versions || true)" + fi + if [[ -z "$resolved" ]]; then + if [[ ! -f go.mod ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY} has no golang pin in .tool-versions and no go.mod to derive one from." + exit 1 + fi + version="$(sed -nE 's/^toolchain[[:space:]]+go([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + if [[ -z "$version" ]]; then + version="$(sed -nE 's/^go[[:space:]]+([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + fi + if [[ -z "$version" ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY}/go.mod declares no usable go version." + exit 1 + fi + if [[ "$version" != *.*.* ]]; then + version="${version}.0" + fi + resolved="golang ${version}" + echo "::notice title=Derived go toolchain::Using ${resolved} from ${WORKING_DIRECTORY}/go.mod." + fi + { + echo "tool_versions<> "$GITHUB_OUTPUT" + + - name: Set up asdf-managed Go + 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: Build declared distributions + env: + RELEASE: ${{ toJSON(matrix.release) }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + artifact_command="$(jq -er '.artifact_command' "$GITHUB_WORKSPACE/$COMPONENT_DESCRIPTOR")" + rm -rf dist + bash -c "$artifact_command" + if ! ls dist/* > /dev/null 2>&1; then + echo "::error title=Missing artifacts::artifact_command produced no dist/* for ${RELEASE}." + exit 1 + fi + + - name: Upload release distributions + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: go-distributions-${{ matrix.release.tag }} + path: ${{ env.WORKING_DIRECTORY }}/dist/* + if-no-files-found: error + retention-days: 14 + + finalize-release: + name: Finalize release (promote + sync PR) + 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: 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 }} + STAGING_REF: ${{ needs.prepare-release.outputs.staging_ref }} + 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="$(jq -r '.[0].tag' <<< "$RELEASES")" + version="$(jq -r '.[0].version' <<< "$RELEASES")" + git check-ref-format "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" + + git push --atomic origin "${RELEASE_COMMIT}:${EXPECTED_REF}" "refs/tags/$tag" + + - name: Open sync-to-develop pull request + if: steps.promote.outcome == 'success' && vars.DEVELOPMENT_FLOW == 'git-flow' + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + BASELINE_BRANCH: ${{ github.event.pull_request.base.ref || github.ref_name }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASES: ${{ needs.prepare-release.outputs.releases }} + 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" + + 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 }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${STAGING_REF:-}" ]]; then + exit 0 + fi + gh auth setup-git + git push origin ":${STAGING_REF}" || true diff --git a/.github/workflows/code-gomod_go-release.yml b/.github/workflows/code-gomod_go-release.yml new file mode 100644 index 0000000..b66f0e7 --- /dev/null +++ b/.github/workflows/code-gomod_go-release.yml @@ -0,0 +1,98 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: code-gomod-go-release +run-name: Create Go GitHub release from ${{ github.event_name }} + +on: + pull_request: + types: [closed] + branches: [main, main-*] + workflow_dispatch: + inputs: + release_type: + description: Release type to use + required: true + default: release-type/minor + type: choice + options: + - release-type/hotfix + - release-type/multi-hotfix + - release-type/major + - release-type/minor + - release-type/patch + +permissions: + actions: read + contents: read + +concurrency: + group: code-gomod-go-release-${{ github.repository }}-${{ github.event.pull_request.base.ref || github.ref_name }} + cancel-in-progress: false + +jobs: + release-core: + name: Prepare and Build Release + uses: ./.github/workflows/code-gomod_go-release-core.yml + with: + release_type: ${{ inputs.release_type }} + secrets: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + + github-release: + name: Create GitHub Release ${{ matrix.release.tag }} + needs: release-core + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + release: ${{ fromJSON(needs.release-core.outputs.releases) }} + 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 + + - name: Download release distributions + if: needs.release-core.outputs.artifacts == 'binaries' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: go-distributions-${{ matrix.release.tag }} + path: dist + + - name: Create or update GitHub Release + env: + ARTIFACTS: ${{ needs.release-core.outputs.artifacts }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASE_TAG: ${{ matrix.release.tag }} + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + assets=() + if [[ "$ARTIFACTS" == "binaries" ]]; then + assets=(dist/*) + if [[ ${#assets[@]} -eq 0 ]]; then + echo "::error title=Missing distributions::artifacts: binaries produced an empty release artifact." + exit 1 + fi + fi + + if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then + if [[ ${#assets[@]} -gt 0 ]]; then + gh release upload "$RELEASE_TAG" "${assets[@]}" \ + --repo "$GITHUB_REPOSITORY" \ + --clobber + fi + else + gh release create "$RELEASE_TAG" "${assets[@]}" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$RELEASE_TAG" \ + --generate-notes + fi diff --git a/.github/workflows/code-gomod_go-sonarcloud-analysis.yml b/.github/workflows/code-gomod_go-sonarcloud-analysis.yml new file mode 100644 index 0000000..0dd3c8d --- /dev/null +++ b/.github/workflows/code-gomod_go-sonarcloud-analysis.yml @@ -0,0 +1,145 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: code-gomod-go-sonarcloud-analysis +run-name: SonarCloud analysis on ${{ github.head_ref || github.ref_name }} + +on: + workflow_dispatch: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, main-*, develop, develop-*] + push: + branches: [main, main-*, develop, develop-*] + +permissions: + contents: read + +concurrency: + group: code-gomod-go-sonarcloud-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +env: + WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} + ASDF_BRANCH_VERSION: '0.18.0' + +jobs: + analyze: + name: SonarCloud / Analyze + if: >- + vars.SONARCLOUD_ENABLED == 'true' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && + ( + ( + 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 + steps: + - name: Resolve SonarCloud availability + id: sonar + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${SONAR_TOKEN:-}" ]]; then + echo "::notice title=SonarCloud skipped::SONAR_TOKEN is not configured; skipping advisory analysis." + echo "enabled=false" >> "$GITHUB_OUTPUT" + else + echo "enabled=true" >> "$GITHUB_OUTPUT" + fi + + - name: Check out source + if: steps.sonar.outputs.enabled == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Read governed tool versions + id: tool-versions + if: steps.sonar.outputs.enabled == 'true' + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + resolved="" + if [[ -f .tool-versions ]]; then + if grep -vE '^[[:space:]]*(#|$)' .tool-versions | grep -qvE '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$'; then + echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." + exit 1 + fi + resolved="$(grep -E '^golang ' .tool-versions || true)" + fi + if [[ -z "$resolved" ]]; then + if [[ ! -f go.mod ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY} has no golang pin in .tool-versions and no go.mod to derive one from." + exit 1 + fi + version="$(sed -nE 's/^toolchain[[:space:]]+go([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + if [[ -z "$version" ]]; then + version="$(sed -nE 's/^go[[:space:]]+([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + fi + if [[ -z "$version" ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY}/go.mod declares no usable go version." + exit 1 + fi + if [[ "$version" != *.*.* ]]; then + version="${version}.0" + fi + resolved="golang ${version}" + echo "::notice title=Derived go toolchain::Using ${resolved} from ${WORKING_DIRECTORY}/go.mod." + fi + { + echo "tool_versions<> "$GITHUB_OUTPUT" + + - name: Set up asdf-managed Go + if: steps.sonar.outputs.enabled == 'true' + 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: Generate Go coverage profile + if: steps.sonar.outputs.enabled == 'true' + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + go test -covermode=atomic -coverprofile=coverage.out ./... \ + || echo "::warning title=Coverage unavailable::go test produced no coverage profile; Sonar will analyze without coverage." + + - name: Run SonarCloud analysis + if: steps.sonar.outputs.enabled == 'true' + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 + env: + SONAR_HOST_URL: https://sonarcloud.io + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: >- + -Dsonar.organization=inditextech + -Dsonar.projectKey=InditexTech_${{ github.event.repository.name }} + -Dsonar.projectName=${{ github.event.repository.name }} + -Dsonar.sources=${{ env.WORKING_DIRECTORY }} + -Dsonar.tests=${{ env.WORKING_DIRECTORY }} + -Dsonar.test.inclusions=**/*_test.go + -Dsonar.exclusions=**/vendor/**,**/dist/** + -Dsonar.go.coverage.reportPaths=${{ env.WORKING_DIRECTORY }}/coverage.out diff --git a/.github/workflows/code-release_preview.yml b/.github/workflows/code-release_preview.yml new file mode 100644 index 0000000..e0c2f71 --- /dev/null +++ b/.github/workflows/code-release_preview.yml @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: 2026 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.) +# SPDX-License-Identifier: Apache-2.0 + +name: code-release-preview + +on: + pull_request: + types: [labeled, synchronize, ready_for_review, opened, reopened] + branches: [main, main-*, develop, develop-*] + +permissions: + contents: read + +concurrency: + group: code-release-preview-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +env: + WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} + +jobs: + release-preview: + name: Release Preview + if: >- + github.event.pull_request.draft == false && + github.event.pull_request.head.repo.full_name == github.repository && + ( + (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-'))) + ) + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + pull-requests: write + steps: + - name: Check out pull request head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + + - name: Check CHANGELOG has Unreleased changes + if: >- + !contains(github.event.pull_request.labels.*.name, 'skip-release') && + 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 }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + if git diff --quiet "origin/$BASE_REF"...HEAD -- CHANGELOG.md; then + echo "::error title=No CHANGELOG changes::Update the Unreleased section with this release's changes." + gh pr comment "$GITHUB_EVENT_NUMBER" --repo "$GITHUB_REPOSITORY" --body $'### :x: No changes in `CHANGELOG.md`\nUpdate the `## [Unreleased]` section listing the changes for this release before merging.' + exit 1 + fi + + - name: Calculate release preview + env: + DEVELOPMENT_FLOW: ${{ vars.DEVELOPMENT_FLOW }} + GITHUB_EVENT_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ github.token }} + RELEASE_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + + if [[ ",$RELEASE_LABELS," == *",skip-release,"* ]]; then + body=$'### Release Preview\n\nThis pull request has the `skip-release` label and will not create a GitHub release.' + gh pr comment "$GITHUB_EVENT_NUMBER" \ + --repo "$GITHUB_REPOSITORY" \ + --body "$body" + exit 0 + 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 + body=$'### Release Preview\n\nAdd a `release-type/...` label to calculate a release for this git-flow pull request.' + gh pr comment "$GITHUB_EVENT_NUMBER" \ + --repo "$GITHUB_REPOSITORY" \ + --body "$body" + exit 0 + fi + ;; + esac + + merge_strategy="Create a merge commit" + if [[ "$DEVELOPMENT_FLOW" == "trunk-based-development" ]]; then + merge_strategy="Squash and merge" + 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" + } + + current_version="0.0.0" + has_release="false" + while IFS= read -r tag; do + [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || continue + current_version="${tag#v}" + has_release="true" + break + done < <(git tag --list 'v*' --sort=-v:refname) + + release_version="$(bump_semver "$current_version" "$release_bump")" + release_tag="v${release_version}" + + if [[ "$has_release" == "true" ]]; then + current_label="\`v$current_version\`" + else + current_label="_none (first release)_" + fi + versions="$(printf '%s\n' \ + "- Current version (latest tag): $current_label" \ + "- Proposed release tag: \`$release_tag\`" \ + "- Next development version: CHANGELOG \`## [Unreleased]\` re-seeded on the release commit (tag-only, no in-tree version)")" + + changes="$(awk ' + BEGIN { capture=0 } + /^## \[[Uu]nreleased\]/ { capture=1; next } + capture && /^## \[/ { capture=0 } + capture { print } + ' CHANGELOG.md)" + + body="$(printf "%s\n\n%s\n- Release type: \`%s\`\n- Expected merge strategy: **%s**\n\n#### Changes\n%s" \ + '### Go Release Preview' \ + "$versions" \ + "$release_bump" \ + "$merge_strategy" \ + "$changes")" + gh pr comment "$GITHUB_EVENT_NUMBER" \ + --repo "$GITHUB_REPOSITORY" \ + --body "$body" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..8985bbf --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,155 @@ +# 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 + +env: + WORKING_DIRECTORY: ${{ vars.WORKING_DIRECTORY }} + ASDF_BRANCH_VERSION: '0.18.0' + +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: go + build-mode: autobuild + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Read governed tool versions + id: tool-versions + if: matrix.language == 'go' + working-directory: ${{ env.WORKING_DIRECTORY }} + shell: bash + run: | + set -euo pipefail + resolved="" + if [[ -f .tool-versions ]]; then + if grep -vE '^[[:space:]]*(#|$)' .tool-versions | grep -qvE '^[a-zA-Z0-9_-]+ [a-zA-Z0-9._+-]+$'; then + echo "::error title=Invalid tool-versions::${WORKING_DIRECTORY}/.tool-versions is malformed." + exit 1 + fi + resolved="$(grep -E '^golang ' .tool-versions || true)" + fi + if [[ -z "$resolved" ]]; then + if [[ ! -f go.mod ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY} has no golang pin in .tool-versions and no go.mod to derive one from." + exit 1 + fi + version="$(sed -nE 's/^toolchain[[:space:]]+go([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + if [[ -z "$version" ]]; then + version="$(sed -nE 's/^go[[:space:]]+([0-9]+\.[0-9]+(\.[0-9]+)?)[[:space:]]*$/\1/p' go.mod | head -n 1)" + fi + if [[ -z "$version" ]]; then + echo "::error title=Missing go toolchain::${WORKING_DIRECTORY}/go.mod declares no usable go version." + exit 1 + fi + if [[ "$version" != *.*.* ]]; then + version="${version}.0" + fi + resolved="golang ${version}" + echo "::notice title=Derived go toolchain::Using ${resolved} from ${WORKING_DIRECTORY}/go.mod." + fi + { + echo "tool_versions<> "$GITHUB_OUTPUT" + + - name: Set up asdf-managed Go + if: matrix.language == 'go' + 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: Initialize CodeQL + uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4 + with: + build-mode: ${{ matrix.build-mode }} + languages: ${{ matrix.language }} + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # 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 0000000..66c196d --- /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 0000000..b5c40f9 --- /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 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/.sync-exclude b/.sync-exclude new file mode 100644 index 0000000..e360573 --- /dev/null +++ b/.sync-exclude @@ -0,0 +1,5 @@ +# 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. +CONTRIBUTING.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2807d9c..fdf1b87 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,13 @@ + + # Code of Conduct By participating in this project, you agree to abide by the rules and principles outlined in the Code of Conduct. Please review it to understand the expectations for respectful and inclusive collaboration. This project adheres to the general [Inditex Tech Code of Conduct](https://github.com/InditexTech/foss/blob/main/CODE_OF_CONDUCT.md). + +For any inquiries regarding this Code of Conduct, please contact us at oso@inditex.com. diff --git a/SECURITY.md b/SECURITY.md index 364ce71..f3d94d1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,24 +1,41 @@ -# Security - -We at Inditex believe that responsible disclosure of security vulnerabilities helps us ensure the security and privacy -of all opensource community. - -If you believe you have found a security vulnerability in any Inditex repository that meets Inditex definition of a -security vulnerability, please report it to us as described below. We appreciate the hard work maintainers put into -fixing vulnerabilities and understand that sometimes more time is required to properly address an issue. - -## Reporting security issues - -> [!CAUTION] -> Do not file public issues on GitHub for security vulnerabilities - -* Let us know by submitting the finding through our [disclosure submission program](https://vdp.inditex.com) -as soon as possible, upon discovery of a potential security issue. -* Once we've assessed your report, we will create a GitHub "security advisory", which will allow the reporter and -Inditex team to work on the issue in a confidential manner. We will invite you as a collaborator to the advisory and any -needed trusted persons. -* That "security advisory" will also allow us to have a temporary private fork, to work on the fix in confidentiality. -* Once a fix is ready, we will include the fix in our next release and mark that release as a security release. -* Details on the issue will be embargoed for 30 days to give users an oppurtunity to upgrade, after which we will -coordinate disclosure with the researcher(s). -* If you've contributed the fix, you will be credited for it. + + +# Security Policy + +## Reporting a Vulnerability + +We take the security of the project seriously. If you believe you have found a security vulnerability, please report it to us as described below. + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them via our [disclosure submission program](https://vdp.inditex.com). + +## Preferred Languages + +We prefer all communications to be in English. + +## Process + +1. Security report received +2. Security team acknowledges receipt within 48 hours +3. Team investigates and determines severity +4. Team develops and tests fix +5. Team prepares advisory and patches +6. Advisory published, patches released + +## Safe Harbor + +We support safe harbor for security researchers who: + +1. Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services +2. Only interact with accounts you own or with explicit permission of the account holder +3. Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party +4. Do not exploit a security issue for purposes other than immediate testing + +## Third-party Security Notifications + +We review security reports for our dependencies and follow responsible disclosure guidelines. diff --git a/repolinter.json b/repolinter.json index 499ca3c..4e64643 100644 --- a/repolinter.json +++ b/repolinter.json @@ -51,16 +51,6 @@ } } }, - "changelog-file-exists": { - "level": "error", - "rule": { - "type": "file-existence", - "options": { - "globsAny": ["CHANGELOG*"], - "nocase": true - } - } - }, "security-file-exists": { "level": "error", "rule": { @@ -70,18 +60,8 @@ } } }, - "support-file-exists": { - "level": "off", - "rule": { - "type": "file-existence", - "options": { - "globsAny": ["{docs/,.github/,}SUPPORT*"], - "nocase": true - } - } - }, "readme-references-license": { - "level": "off", + "level": "error", "rule": { "type": "file-contents", "options": { @@ -100,38 +80,19 @@ } } }, - "test-directory-exists": { - "level": "off", - "rule": { - "type": "directory-existence", - "options": { - "globsAny": ["/test", "/specs"], - "nocase": true - } - } - }, "integrates-with-ci": { "level": "error", "rule": { "type": "file-existence", "options": { "globsAny": [ - ".gitlab-ci.yml", - ".travis.yml", - "appveyor.yml", - ".appveyor.yml", - "circle.yml", - ".circleci/config.yml", - "Jenkinsfile", - ".drone.yml", - ".github/workflows/", - "azure-pipelines.yml" + ".github/workflows/" ] } } }, "code-of-conduct-file-contains-email": { - "level": "off", + "level": "error", "rule": { "type": "file-contents", "options": { @@ -149,18 +110,6 @@ } } }, - "source-license-headers-exist": { - "level": "warning", - "rule": { - "type": "file-starts-with", - "options": { - "globsAll": ["./**/*.go"], - "lineCount": 5, - "patterns": ["Copyright", "License"], - "flags": "i" - } - } - }, "github-issue-template-exists": { "level": "error", "rule": { @@ -172,7 +121,7 @@ } }, "github-pull-request-template-exists": { - "level": "off", + "level": "error", "rule": { "type": "file-existence", "options": { @@ -190,7 +139,7 @@ "rule": { "type": "file-existence", "options": { - "globsAny": ["package.json"] + "globsAny": ["{,**/}package.json"] } } }, @@ -210,7 +159,7 @@ "rule": { "type": "file-existence", "options": { - "globsAny": ["pom.xml", "build.xml", "build.gradle"] + "globsAny": ["{,**/}pom.xml", "{,**/}build.xml", "{,**/}build.gradle"] } } }, @@ -220,7 +169,11 @@ "rule": { "type": "file-existence", "options": { - "globsAny": ["setup.py", "requirements.txt"] + "globsAny": [ + "{,**/}setup.py", + "{,**/}requirements.txt", + "{,**/}pyproject.toml" + ] } } }, @@ -265,7 +218,7 @@ } }, "license-detectable-by-licensee": { - "level": "off", + "level": "error", "where": ["license="], "rule": { "type": "license-detectable-by-licensee", @@ -283,14 +236,8 @@ } } }, - "best-practices-badge-present": { - "level": "off", - "rule": { - "type": "best-practices-badge-present" - } - }, "internal-file-not-exists": { - "level": "off", + "level": "error", "rule": { "type": "file-not-exists", "options": { @@ -308,4 +255,4 @@ } } } -} +} \ No newline at end of file From 6880141048059c0978500519fb4836e1ca85fc61 Mon Sep 17 00:00:00 2001 From: Ivan Alvarez Sabin Date: Fri, 11 Sep 2026 01:07:57 +0200 Subject: [PATCH 2/7] ci: drop workflows and issue templates superseded by CI governance --- .github/ISSUE_TEMPLATE/bug-issue.md | 16 ------ .github/ISSUE_TEMPLATE/feature-issue.md | 16 ------ .github/workflows/PR-verify.yml | 76 ------------------------- .github/workflows/release.yml | 43 -------------- 4 files changed, 151 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-issue.md delete mode 100644 .github/ISSUE_TEMPLATE/feature-issue.md delete mode 100644 .github/workflows/PR-verify.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/ISSUE_TEMPLATE/bug-issue.md b/.github/ISSUE_TEMPLATE/bug-issue.md deleted file mode 100644 index 867cbed..0000000 --- a/.github/ISSUE_TEMPLATE/bug-issue.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Bug Report -about: Use this template to report a bug -title: '' -labels: kind/bug -assignees: '' - ---- - -### Detailed description - -A clear and concise description of what the problem is. - -### Expected behaviour - -Expected behaviour one the problem is fixed. diff --git a/.github/ISSUE_TEMPLATE/feature-issue.md b/.github/ISSUE_TEMPLATE/feature-issue.md deleted file mode 100644 index 2699c53..0000000 --- a/.github/ISSUE_TEMPLATE/feature-issue.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Feature request -about: Suggest an idea/feature for this project -title: '' -labels: 'kind/feature' -assignees: '' - ---- - -### Motivation - -Describe here the motivation of the request. - -### Acceptance criteria - -- [ ] A check list of tasks to be done to assume the issue addressed diff --git a/.github/workflows/PR-verify.yml b/.github/workflows/PR-verify.yml deleted file mode 100644 index 69916fa..0000000 --- a/.github/workflows/PR-verify.yml +++ /dev/null @@ -1,76 +0,0 @@ ---- -name: PR-verify - -on: - pull_request: - -env: - GO_VERSION: 1.25.13 - NODE_VERSION: 18.x - -jobs: - verify: - name: Verify - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: KengoTODA/actions-setup-docker-compose@v1 - with: - version: '2.14.2' # the full version of `docker-compose` command - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - - name: Setup Go version - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Verify - run: | - make verify - - repo-linter: - name: Repo Linter - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - - name: Setup Node version - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Execute repolinter - run: | - echo "Installing repolinter" - npm install -g repolinter@v0.11.2 - - echo "Executing repolinter" - repolinter --rulesetFile repolinter.json --dryRun . - - echo "Repolinter execution completed" - - reuse-compliance: - name: REUSE Compliance - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - - name: REUSE Compliance Check - uses: fsfe/reuse-action@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index f423ea4..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Release - -on: - workflow_dispatch: - pull_request: - types: - - closed - -jobs: - release: - name: Release - runs-on: ubuntu-latest - permissions: - contents: write - if: ${{ (github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'kind/release')) || github.event_name == 'workflow_dispatch' }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Retrieve version - shell: bash - run: | - echo "VERSION=$(make get-version)" >> "$GITHUB_ENV" - - - name: Create Git Tag - run: | - git config user.email "ghbot@github.com" - git config user.name "GitHub Bot" - - git tag -a "${{ env.VERSION }}" -m "Version ${{ env.VERSION }}" - git push origin "${{ env.VERSION }}" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create GitHub Release - uses: ncipollo/release-action@a2e71bdd4e7dab70ca26a852f29600c98b33153e - with: - name: v${{ env.VERSION }} - tag: ${{ env.VERSION }} - token: ${{ secrets.GITHUB_TOKEN }} - generateReleaseNotes: true From 515a33c6e63f86731e0303bf7cfee1d7347fd8fd Mon Sep 17 00:00:00 2001 From: Ivan Alvarez Sabin Date: Fri, 11 Sep 2026 01:07:57 +0200 Subject: [PATCH 3/7] fix: install Go tools into a repository-local directory --- Makefile | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index ea6febc..0690931 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ PROJECT_VERSION := 1.0.0 -GOPATH := $(shell command go env GOPATH) +TOOLS_BIN := $(CURDIR)/.bin XK6_VERSION := v0.13.4 -XK6_BINARY := "$(GOPATH)/bin/xk6" +XK6_BINARY := "$(TOOLS_BIN)/xk6" GOLANGCI_VERSION := v1.64.5 -GOLANGCI_BINARY := "$(GOPATH)/bin/golangci-lint" +GOLANGCI_BINARY := "$(TOOLS_BIN)/golangci-lint" .DEFAULT_GOAL := all @@ -15,16 +15,17 @@ all: format lint compose-up test run compose-down .PHONY: deps deps: + @mkdir -p "$(TOOLS_BIN)" @if [ ! -f "$(XK6_BINARY)" ]; then \ echo "Installing xk6..."; \ - go install go.k6.io/xk6/cmd/xk6@$(XK6_VERSION); \ + GOBIN="$(TOOLS_BIN)" go install go.k6.io/xk6/cmd/xk6@$(XK6_VERSION); \ else \ echo "xk6 is already installed."; \ fi @if [ ! -f "$(GOLANGCI_BINARY)" ]; then \ echo "Installing golangci-lint..."; \ - go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_VERSION); \ + GOBIN="$(TOOLS_BIN)" go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_VERSION); \ else \ echo "golangci-lint is already installed."; \ fi From a4a3fe07cb954c240d77ab4c5bff817c504558c3 Mon Sep 17 00:00:00 2001 From: Ivan Alvarez Sabin Date: Fri, 11 Sep 2026 01:08:01 +0200 Subject: [PATCH 4/7] docs: adopt keep-a-changelog format for CHANGELOG.md --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b0f9a..534f355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog -All the versions released and their respective changelogs are available in the [Releases](../../releases) section of this repository. +All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](https://semver.org/). \ No newline at end of file +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/). + +## [Unreleased] + +## [1.0.0] - 2025-04-08 + +### Added + +- Initial release. + +[unreleased]: https://github.com/InditexTech/xk6-sftp/compare/1.0.0...HEAD +[1.0.0]: https://github.com/InditexTech/xk6-sftp/releases/tag/1.0.0 \ No newline at end of file From 239c6cba742b3206ca7680f7b8e1776d861a8fa6 Mon Sep 17 00:00:00 2001 From: Ivan Alvarez Sabin Date: Fri, 11 Sep 2026 01:08:01 +0200 Subject: [PATCH 5/7] chore: annotate .sync-exclude for REUSE compliance --- REUSE.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/REUSE.toml b/REUSE.toml index 622d03b..784e76f 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -14,7 +14,8 @@ path = [ "examples/**", "repolinter.json", "REUSE.toml", - "Makefile" + "Makefile", + ".sync-exclude" ] SPDX-FileCopyrightText = "2025 INDUSTRIA DE DISEÑO TEXTIL S.A. (INDITEX S.A.)" SPDX-License-Identifier = "Apache-2.0" \ No newline at end of file From 4ead0f90a703b7247f21ea1a70e0acc06aa35c41 Mon Sep 17 00:00:00 2001 From: Ivan Alvarez Sabin Date: Fri, 11 Sep 2026 01:08:01 +0200 Subject: [PATCH 6/7] ci: scope Dependabot to product-owned dependencies --- .github/dependabot.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 150728a..452f3d1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,15 +9,7 @@ updates: time: '04:00' timezone: UTC open-pull-requests-limit: 10 - - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - day: monday - time: '04:00' - timezone: UTC - open-pull-requests-limit: 5 + skip-release: true - package-ecosystem: docker-compose directory: /docker @@ -27,3 +19,4 @@ updates: time: '04:00' timezone: UTC open-pull-requests-limit: 5 + skip-release: true From 326586b0f8ed8077b0240fd9f5df4e4abbb3beba Mon Sep 17 00:00:00 2001 From: Ivan Alvarez Sabin Date: Fri, 11 Sep 2026 01:08:29 +0200 Subject: [PATCH 7/7] fix: use valid Dependabot release-skip labels --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 452f3d1..85a3f24 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,7 +9,7 @@ updates: time: '04:00' timezone: UTC open-pull-requests-limit: 10 - skip-release: true + labels: ["skip-release"] - package-ecosystem: docker-compose directory: /docker @@ -19,4 +19,4 @@ updates: time: '04:00' timezone: UTC open-pull-requests-limit: 5 - skip-release: true + labels: ["skip-release"]