diff --git a/.github/workflows/branch-cleanup.yml b/.github/workflows/branch-cleanup.yml index 25b20639..33c07143 100644 --- a/.github/workflows/branch-cleanup.yml +++ b/.github/workflows/branch-cleanup.yml @@ -51,7 +51,7 @@ jobs: uses: actions/checkout@v6 - name: Run branch cleanup - uses: LerianStudio/github-actions-shared-workflows/src/config/branch-cleanup@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/branch-cleanup@v1.18.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} stale-days: ${{ inputs.stale_days }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 053569ee..77cde1d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -122,6 +122,10 @@ on: description: 'Force multi-platform build (amd64+arm64) even for beta/rc tags' type: boolean default: false + build_context_from_working_dir: + description: 'Use the component working_dir as Docker build context instead of build_context. Useful for independent modules (e.g., tools with their own go.mod).' + type: boolean + default: false permissions: contents: read @@ -139,7 +143,7 @@ jobs: - name: Get changed paths (monorepo) if: inputs.filter_paths != '' id: changed-paths - uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.18.0 with: filter-paths: ${{ inputs.filter_paths }} shared-paths: ${{ inputs.shared_paths }} @@ -281,7 +285,7 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v7 with: - context: ${{ inputs.build_context }} + context: ${{ inputs.build_context_from_working_dir == true && matrix.app.working_dir || inputs.build_context }} file: ${{ matrix.app.working_dir }}/${{ inputs.dockerfile_name }} platforms: ${{ needs.prepare.outputs.platforms }} push: true diff --git a/.github/workflows/gitops-update.yml b/.github/workflows/gitops-update.yml index 024a8ee8..7052345d 100644 --- a/.github/workflows/gitops-update.yml +++ b/.github/workflows/gitops-update.yml @@ -52,9 +52,9 @@ on: type: string default: 'v4.44.3' enable_docker_login: - description: 'Enable Docker Hub login to avoid rate limits' + description: 'Enable Docker Hub login to avoid rate limits. Disabled by default since GitOps updates do not require Docker registry access.' type: boolean - default: true + default: false configmap_updates: description: 'JSON object mapping artifact names to configmap keys (e.g., {"pix.tag": ".pix.configmap.VERSION"})' type: string @@ -104,7 +104,7 @@ jobs: # Determine which servers to deploy to DEPLOY_FIRMINO="${{ inputs.deploy_in_firmino }}" DEPLOY_CLOTILDE="${{ inputs.deploy_in_clotilde }}" - + echo "deploy_firmino=$DEPLOY_FIRMINO" >> "$GITHUB_OUTPUT" echo "deploy_clotilde=$DEPLOY_CLOTILDE" >> "$GITHUB_OUTPUT" echo "Deploy to Firmino: $DEPLOY_FIRMINO" @@ -144,7 +144,7 @@ jobs: shell: bash run: | set -e - cd gitops + cd gitops || exit 1 git pull origin main - name: Cleanup old artifacts (self-hosted runner safety) @@ -179,17 +179,17 @@ jobs: shell: bash run: | echo "Checking for downloaded artifacts..." - + # Check if directory exists and has files if [[ ! -d ".gitops-tags" ]]; then echo "::error::No artifacts directory found. Build job may have been skipped or failed to produce artifacts." echo "artifacts_valid=false" >> "$GITHUB_OUTPUT" exit 1 fi - + # Count actual artifact files (not directories) ARTIFACT_COUNT=$(find .gitops-tags -type f -name "*.tag" 2>/dev/null | wc -l || echo "0") - + if [[ "$ARTIFACT_COUNT" -eq 0 ]]; then echo "::error::No artifact files found in .gitops-tags/. Build job may have been skipped or failed." echo "Directory contents:" @@ -197,7 +197,7 @@ jobs: echo "artifacts_valid=false" >> "$GITHUB_OUTPUT" exit 1 fi - + echo "Found $ARTIFACT_COUNT artifact file(s):" ls -la .gitops-tags/ echo "artifacts_valid=true" >> "$GITHUB_OUTPUT" @@ -210,7 +210,7 @@ jobs: # Get app name APP_NAME="${{ steps.setup.outputs.app_name }}" - + # Determine environments to update based on tag type if [[ "${{ env.IS_BETA }}" == "true" ]]; then ENVIRONMENTS="dev" @@ -244,7 +244,7 @@ jobs: SERVERS="clotilde" fi fi - + if [[ -z "$SERVERS" ]]; then echo "No servers selected for deployment. Enable deploy_in_firmino or deploy_in_clotilde." exit 1 @@ -262,7 +262,7 @@ jobs: AVAILABLE_ARTIFACTS="${AVAILABLE_ARTIFACTS}${artifact_name} " fi done - + if [[ -z "$AVAILABLE_ARTIFACTS" ]]; then echo "No artifacts found in .gitops-tags/" echo "sync_matrix=[]" >> "$GITHUB_OUTPUT" @@ -280,10 +280,10 @@ jobs: local file="$1" local yaml_path="$2" local new_tag="$3" - + # Extract the key name from yaml path (e.g., ".image.tag" -> "tag") local key_name="${yaml_path##*.}" - + # Use sed to replace only the tag value, preserving all formatting # This handles patterns like "tag: v1.0.0" or "tag: 'v1.0.0'" or 'tag: "v1.0.0"' if grep -q "^[[:space:]]*${key_name}:" "$file"; then @@ -298,21 +298,21 @@ jobs: for SERVER in $SERVERS; do for ENV in $ENVIRONMENTS; do VALUES_FILE="gitops/environments/${SERVER}/helmfile/applications/${ENV}/${APP_NAME}/values.yaml" - + echo "" echo "Processing: $SERVER/$ENV" echo " Path: $VALUES_FILE" - + # Check if file exists if [[ ! -f "$VALUES_FILE" ]]; then echo " WARNING: Values file not found for ${SERVER}/${ENV}: ${VALUES_FILE}" MISSING_FILES="${MISSING_FILES}${SERVER}/${ENV}:${VALUES_FILE}\n" continue fi - + # Track if any changes were made to this file FILE_CHANGED=false - + # Apply mappings from inputs - only if artifact exists MAPPINGS='${{ inputs.yaml_key_mappings }}' while IFS='|' read -r artifact_key yaml_key; do @@ -334,7 +334,7 @@ jobs: fi fi done < <(echo "$MAPPINGS" | jq -r 'to_entries[] | "\(.key)|\(.value)"') - + # Apply configmap updates if configured - only if artifact exists if [[ -n "${{ inputs.configmap_updates }}" ]]; then CONFIGMAP_MAPPINGS='${{ inputs.configmap_updates }}' @@ -356,7 +356,7 @@ jobs: fi done < <(echo "$CONFIGMAP_MAPPINGS" | jq -r 'to_entries[] | "\(.key)|\(.value)"') fi - + # Only track as updated if changes were actually made if [[ "$FILE_CHANGED" == "true" ]]; then UPDATED_FILES="${UPDATED_FILES}${VALUES_FILE}\n" @@ -378,7 +378,7 @@ jobs: else echo "No files were updated" fi - + if [[ -n "$MISSING_FILES" ]]; then echo "" echo "Missing files (skipped):" @@ -388,7 +388,7 @@ jobs: # Save updated servers/envs for ArgoCD sync - only include files that actually changed if [[ -n "$UPDATED_SERVERS_ENVS" ]]; then echo -e "$UPDATED_SERVERS_ENVS" | grep -v '^$' > /tmp/updated_servers_envs.txt - + # Output sync targets as JSON array for matrix job SYNC_MATRIX=$(echo -e "$UPDATED_SERVERS_ENVS" | grep -v '^$' | while IFS=: read -r s e; do [[ -n "$s" && -n "$e" ]] && echo "{\"server\":\"$s\",\"env\":\"$e\"}" @@ -400,7 +400,7 @@ jobs: echo "sync_matrix=[]" >> "$GITHUB_OUTPUT" echo "has_sync_targets=false" >> "$GITHUB_OUTPUT" fi - + COUNT=0 if [[ -n "$UPDATED_FILES" ]]; then COUNT=$(echo -e "$UPDATED_FILES" | grep -c -v '^$' || true) @@ -410,7 +410,7 @@ jobs: - name: Show git diff shell: bash run: | - cd gitops + cd gitops || exit 1 echo "Changes to be committed:" git diff --stat echo "" @@ -432,17 +432,17 @@ jobs: - name: Commit & push (GitOps) run: | set -e - cd gitops - + cd gitops || exit 1 + # Get env label from apply_tags step ENV_LABEL="${{ steps.apply_tags.outputs.env_label }}" - + # Check if there are changes to commit if git diff --quiet; then echo "No changes to commit" exit 0 fi - + git commit -am "ci(${{ steps.setup.outputs.commit_prefix }}): update image tags ($ENV_LABEL)" || echo "No changes to commit" git push origin main diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 1ba3ad27..a88368ef 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -126,7 +126,7 @@ jobs: name: coverage-report - name: Go Coverage Comment - uses: fgrosse/go-coverage-report@v1.2.0 + uses: fgrosse/go-coverage-report@v1.3.0 with: coverage-artifact-name: coverage-report coverage-file-name: coverage.txt diff --git a/.github/workflows/go-pr-analysis.yml b/.github/workflows/go-pr-analysis.yml index fa00ced1..f016b67d 100644 --- a/.github/workflows/go-pr-analysis.yml +++ b/.github/workflows/go-pr-analysis.yml @@ -333,7 +333,7 @@ jobs: - name: Run Gosec for SARIF id: gosec-sarif - uses: securego/gosec@v2.24.7 + uses: securego/gosec@v2.25.0 with: args: -no-fail -fmt sarif -out gosec-${{ matrix.app.name }}.sarif ./${{ matrix.app.working_dir }}/... diff --git a/.github/workflows/gptchangelog.yml b/.github/workflows/gptchangelog.yml index 7acc6177..180b8bf3 100644 --- a/.github/workflows/gptchangelog.yml +++ b/.github/workflows/gptchangelog.yml @@ -146,7 +146,7 @@ jobs: - name: Get changed paths (monorepo) if: (steps.check-tag.outputs.is_stable == 'true' || inputs.stable_releases_only == false) && inputs.filter_paths != '' id: changed-paths - uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.18.0 with: filter-paths: ${{ inputs.filter_paths }} shared-paths: ${{ inputs.shared_paths }} diff --git a/.github/workflows/labels-sync.yml b/.github/workflows/labels-sync.yml index 0b072e89..81f58623 100644 --- a/.github/workflows/labels-sync.yml +++ b/.github/workflows/labels-sync.yml @@ -44,7 +44,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Sync labels - uses: LerianStudio/github-actions-shared-workflows/src/config/labels-sync@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/labels-sync@v1.18.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} config: ${{ inputs.config || '.github/labels.yml' }} diff --git a/.github/workflows/pr-security-scan.yml b/.github/workflows/pr-security-scan.yml index 77910876..4679843d 100644 --- a/.github/workflows/pr-security-scan.yml +++ b/.github/workflows/pr-security-scan.yml @@ -67,6 +67,10 @@ on: description: 'Enable Docker Hub Health Score compliance checks (non-root user, CVEs, licenses)' type: boolean default: true + build_context_from_working_dir: + description: 'Use the component working_dir as Docker build context instead of repo root. Useful for independent modules (e.g., tools with their own go.mod).' + type: boolean + default: false permissions: id-token: write # Required for OIDC authentication @@ -91,7 +95,7 @@ jobs: # ----------------- Detect Changes & Build Matrix ----------------- - name: Get changed paths id: changed-paths - uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.18.0 with: filter-paths: ${{ inputs.filter_paths }} shared-paths: ${{ inputs.shared_paths }} @@ -141,7 +145,7 @@ jobs: - name: Trivy Filesystem Scan id: fs-scan if: always() - uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@v1.18.0 with: scan-ref: ${{ matrix.working_dir }} app-name: ${{ env.APP_NAME }} @@ -151,7 +155,7 @@ jobs: if: always() && inputs.enable_docker_scan uses: docker/build-push-action@v7 with: - context: ${{ inputs.monorepo_type == 'type2' && matrix.working_dir == inputs.frontend_folder && inputs.frontend_folder || '.' }} + context: ${{ inputs.build_context_from_working_dir == true && matrix.working_dir || (inputs.monorepo_type == 'type2' && matrix.working_dir == inputs.frontend_folder && inputs.frontend_folder || '.') }} file: ${{ env.DOCKERFILE_PATH }} platforms: linux/amd64 load: true @@ -165,7 +169,7 @@ jobs: - name: Trivy Image Scan id: image-scan if: always() && inputs.enable_docker_scan - uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-image-scan@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-image-scan@v1.18.0 with: image-ref: '${{ env.DOCKERHUB_ORG }}/${{ env.APP_NAME }}:pr-scan-${{ github.sha }}' app-name: ${{ env.APP_NAME }} @@ -175,7 +179,7 @@ jobs: - name: Dockerfile Compliance Checks id: dockerfile-checks if: always() && inputs.enable_docker_scan && inputs.enable_health_score - uses: LerianStudio/github-actions-shared-workflows/src/security/dockerfile-checks@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/dockerfile-checks@v1.18.0 with: dockerfile-path: ${{ env.DOCKERFILE_PATH }} @@ -183,7 +187,7 @@ jobs: - name: Post Security Scan Results to PR id: post-results if: always() && github.event_name == 'pull_request' - uses: LerianStudio/github-actions-shared-workflows/src/security/pr-security-reporter@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/pr-security-reporter@v1.18.0 with: github-token: ${{ secrets.MANAGE_TOKEN || secrets.GITHUB_TOKEN }} app-name: ${{ env.APP_NAME }} @@ -215,7 +219,7 @@ jobs: runs-on: ${{ inputs.runner_type }} steps: - name: Slack Notification - uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-notify@develop + uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-notify@v1.18.0 with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} status: ${{ needs.security_scan.result }} diff --git a/.github/workflows/release-notification.yml b/.github/workflows/release-notification.yml index dcda48d3..d025a589 100644 --- a/.github/workflows/release-notification.yml +++ b/.github/workflows/release-notification.yml @@ -138,7 +138,7 @@ jobs: exit 1 fi echo "Resolved release tag: $TAG" - echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> "$GITHUB_OUTPUT" - name: Dry run summary if: ${{ inputs.dry_run }} @@ -162,7 +162,7 @@ jobs: - name: Discord notification if: ${{ env.DISCORD_WEBHOOK_URL != '' }} - uses: LerianStudio/github-actions-shared-workflows/src/notify/discord-release@develop + uses: LerianStudio/github-actions-shared-workflows/src/notify/discord-release@v1.18.0 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} release-tag: ${{ steps.release.outputs.tag }} @@ -174,7 +174,7 @@ jobs: - name: Slack notification if: ${{ env.SLACK_WEBHOOK_URL != '' && inputs.slack_channel != '' }} - uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-release@develop + uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-release@v1.18.0 with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} channel: ${{ inputs.slack_channel }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7179f3de..d25bf101 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: - name: Get changed paths (monorepo) if: inputs.filter_paths != '' id: changed-paths - uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.18.0 with: filter-paths: ${{ inputs.filter_paths }} shared-paths: ${{ inputs.shared_paths }} diff --git a/.github/workflows/s3-upload.yml b/.github/workflows/s3-upload.yml index a9dbf679..d3872bd8 100644 --- a/.github/workflows/s3-upload.yml +++ b/.github/workflows/s3-upload.yml @@ -126,7 +126,7 @@ jobs: upload: runs-on: ${{ inputs.runner_type }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Determine environment id: env @@ -170,7 +170,7 @@ jobs: - name: Configure AWS credentials if: steps.env.outputs.folder != '' - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: ${{ inputs.aws_region }} diff --git a/.github/workflows/self-pr-validation.yml b/.github/workflows/self-pr-validation.yml index c98093f3..f7ff2e48 100644 --- a/.github/workflows/self-pr-validation.yml +++ b/.github/workflows/self-pr-validation.yml @@ -176,6 +176,47 @@ jobs: with: files: ${{ needs.changed-files.outputs.composite_files }} + # ----------------- CodeQL Analysis ----------------- + codeql: + name: CodeQL Analysis + runs-on: blacksmith-4vcpu-ubuntu-2404 + needs: changed-files + if: needs.changed-files.outputs.action_files != '' + permissions: + contents: read + security-events: write + pull-requests: write + actions: read + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Generate CodeQL config for changed files + id: codeql-config + uses: ./src/security/codeql-config + with: + changed-paths: ${{ needs.changed-files.outputs.action_files }} + + - name: Initialize CodeQL + if: steps.codeql-config.outputs.skip != 'true' + uses: ./src/security/codeql-init + with: + languages: actions + config-file: ${{ steps.codeql-config.outputs.config-file }} + + - name: Perform CodeQL Analysis + if: steps.codeql-config.outputs.skip != 'true' + uses: ./src/security/codeql-analyze + with: + category: '/language:actions' + + - name: Post CodeQL Results to PR + if: always() && github.event_name == 'pull_request' && steps.codeql-config.outputs.skip != 'true' + uses: ./src/security/codeql-reporter + with: + github-token: ${{ secrets.MANAGE_TOKEN || github.token }} + languages: actions + # ----------------- Lint Report ----------------- lint-report: name: Lint Report diff --git a/.github/workflows/typescript-build.yml b/.github/workflows/typescript-build.yml index 297b98e3..0415630c 100644 --- a/.github/workflows/typescript-build.yml +++ b/.github/workflows/typescript-build.yml @@ -158,7 +158,7 @@ jobs: - name: Get changed paths (monorepo) if: inputs.components_json == '' && inputs.filter_paths != '' id: changed-paths - uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.18.0 with: filter-paths: ${{ inputs.filter_paths }} shared-paths: ${{ inputs.shared_paths }} @@ -255,7 +255,7 @@ jobs: uses: actions/checkout@v6 - name: Build and push Docker image - uses: LerianStudio/github-actions-shared-workflows/src/build/docker-build-ts@develop + uses: LerianStudio/github-actions-shared-workflows/src/build/docker-build-ts@v1.18.0 with: enable-dockerhub: ${{ inputs.enable_dockerhub }} enable-ghcr: ${{ inputs.enable_ghcr }} diff --git a/.github/workflows/typescript-release.yml b/.github/workflows/typescript-release.yml index 025c9901..e3f2877d 100644 --- a/.github/workflows/typescript-release.yml +++ b/.github/workflows/typescript-release.yml @@ -69,7 +69,7 @@ jobs: - name: Get changed paths (monorepo) if: inputs.filter_paths != '' id: changed-paths - uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@develop + uses: LerianStudio/github-actions-shared-workflows/src/config/changed-paths@v1.18.0 with: filter-paths: ${{ inputs.filter_paths }} shared-paths: ${{ inputs.shared_paths }} diff --git a/src/lint/composite-schema/README.md b/src/lint/composite-schema/README.md index 1095b46b..a51e9baf 100644 --- a/src/lint/composite-schema/README.md +++ b/src/lint/composite-schema/README.md @@ -44,7 +44,7 @@ jobs: uses: actions/checkout@v6 - name: Composite Schema Lint - uses: LerianStudio/github-actions-shared-workflows/src/lint/composite-schema@develop + uses: LerianStudio/github-actions-shared-workflows/src/lint/composite-schema@v1.x.x with: files: "src/lint/my-check/action.yml,src/build/my-build/action.yml" ``` diff --git a/src/lint/readme-check/README.md b/src/lint/readme-check/README.md index 466c02d5..ddd57296 100644 --- a/src/lint/readme-check/README.md +++ b/src/lint/readme-check/README.md @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v6 - name: README Check - uses: LerianStudio/github-actions-shared-workflows/src/lint/readme-check@develop + uses: LerianStudio/github-actions-shared-workflows/src/lint/readme-check@v1.x.x with: files: "src/lint/my-check/action.yml,src/build/my-build/action.yml" ``` diff --git a/src/lint/shellcheck/README.md b/src/lint/shellcheck/README.md index 5f669b4a..7da71543 100644 --- a/src/lint/shellcheck/README.md +++ b/src/lint/shellcheck/README.md @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v6 - name: Shell Check - uses: LerianStudio/github-actions-shared-workflows/src/lint/shellcheck@develop + uses: LerianStudio/github-actions-shared-workflows/src/lint/shellcheck@v1.x.x with: files: ".github/workflows/ci.yml,src/lint/my-check/action.yml" ``` diff --git a/src/notify/discord-release/README.md b/src/notify/discord-release/README.md index 566bec12..4e0551d5 100644 --- a/src/notify/discord-release/README.md +++ b/src/notify/discord-release/README.md @@ -55,7 +55,7 @@ jobs: ### Dry run (preview only) ```yaml -- uses: LerianStudio/github-actions-shared-workflows/src/notify/discord-release@develop +- uses: LerianStudio/github-actions-shared-workflows/src/notify/discord-release@v1.x.x with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} release-tag: ${{ github.event.release.tag_name }} diff --git a/src/notify/pr-lint-reporter/README.md b/src/notify/pr-lint-reporter/README.md index 25d8fc35..acf061ec 100644 --- a/src/notify/pr-lint-reporter/README.md +++ b/src/notify/pr-lint-reporter/README.md @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@v6 - name: Post Lint Report - uses: LerianStudio/github-actions-shared-workflows/src/notify/pr-lint-reporter@develop + uses: LerianStudio/github-actions-shared-workflows/src/notify/pr-lint-reporter@v1.x.x with: github-token: ${{ secrets.MANAGE_TOKEN || github.token }} yamllint-result: ${{ needs.yamllint.result }} diff --git a/src/notify/slack-notify/README.md b/src/notify/slack-notify/README.md index da4a2dcb..7edd247a 100644 --- a/src/notify/slack-notify/README.md +++ b/src/notify/slack-notify/README.md @@ -35,7 +35,7 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - name: Slack Notification - uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-notify@develop + uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-notify@v1.x.x with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} status: ${{ needs.build.result }} diff --git a/src/notify/slack-release/README.md b/src/notify/slack-release/README.md index b50faa1a..368328a8 100644 --- a/src/notify/slack-release/README.md +++ b/src/notify/slack-release/README.md @@ -54,7 +54,7 @@ jobs: ### Dry run (preview only) ```yaml -- uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-release@develop +- uses: LerianStudio/github-actions-shared-workflows/src/notify/slack-release@v1.x.x with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} channel: "releases" diff --git a/src/security/codeql-analyze/README.md b/src/security/codeql-analyze/README.md new file mode 100644 index 00000000..749206b3 --- /dev/null +++ b/src/security/codeql-analyze/README.md @@ -0,0 +1,43 @@ + + + + + +
Lerian

codeql-analyze

+ +Composite action that wraps [`github/codeql-action/analyze`](https://github.com/github/codeql-action) to perform CodeQL analysis and upload SARIF results to the GitHub Security tab. Designed to be paired with [`codeql-init`](../codeql-init/). + +## Inputs + +| Input | Description | Required | Default | +|---|---|:---:|---| +| `category` | Category for SARIF results used for deduplication (e.g., `/language:actions`) | Yes | — | +| `output` | Output directory for SARIF files | No | `../results` | + +## Usage + +### Basic (paired with codeql-init) + +```yaml +steps: + - uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.x.x + with: + languages: actions + + - name: Perform CodeQL Analysis + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-analyze@v1.x.x + with: + category: '/language:actions' +``` + +## Permissions required + +```yaml +permissions: + contents: read + security-events: write # required for SARIF upload to GitHub Security tab + actions: read # required for workflows using private repositories +``` diff --git a/src/security/codeql-analyze/action.yml b/src/security/codeql-analyze/action.yml new file mode 100644 index 00000000..ee2e7904 --- /dev/null +++ b/src/security/codeql-analyze/action.yml @@ -0,0 +1,20 @@ +name: CodeQL Analyze +description: Perform CodeQL analysis and upload SARIF results to GitHub Security tab + +inputs: + category: + description: 'Category for SARIF results used for deduplication (e.g., /language:actions)' + required: true + output: + description: 'Output directory for SARIF files' + required: false + default: '../results' + +runs: + using: composite + steps: + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: ${{ inputs.category }} + output: ${{ inputs.output }} diff --git a/src/security/codeql-config/README.md b/src/security/codeql-config/README.md new file mode 100644 index 00000000..78f11440 --- /dev/null +++ b/src/security/codeql-config/README.md @@ -0,0 +1,45 @@ + + + + + +
Lerian

codeql-config

+ +Composite action that generates a dynamic CodeQL configuration file scoped to changed paths. Ensures CodeQL analyzes only modified files in PRs instead of the entire repository. Designed to run before [`codeql-init`](../codeql-init/). + +## Inputs + +| Input | Description | Required | Default | +|---|---|:---:|---| +| `changed-paths` | Comma or newline-separated list of changed file paths | Yes | — | +| `output-file` | Path where the generated config file will be written | No | `.github/codeql-config-pr.yml` | + +## Outputs + +| Output | Description | +|---|---| +| `config-file` | Path to the generated CodeQL config file | +| `skip` | `true` if no paths were resolved and CodeQL should be skipped | + +## Usage + +### With changed-workflows (this repo) + +```yaml +- name: Generate CodeQL config + id: codeql-config + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-config@v1.x.x + with: + changed-paths: ${{ needs.changed-files.outputs.action_files }} + +- name: Initialize CodeQL + if: steps.codeql-config.outputs.skip != 'true' + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.x.x + with: + languages: actions + config-file: ${{ steps.codeql-config.outputs.config-file }} +``` + +## Permissions required + +No special permissions needed — this composite only generates a file. diff --git a/src/security/codeql-config/action.yml b/src/security/codeql-config/action.yml new file mode 100644 index 00000000..9d0f0e1e --- /dev/null +++ b/src/security/codeql-config/action.yml @@ -0,0 +1,62 @@ +name: CodeQL Config Generator +description: Generates a dynamic CodeQL configuration file scoped to changed paths for PR-only analysis. + +inputs: + changed-paths: + description: 'Comma or newline-separated list of changed file paths or directories to scope the analysis' + required: true + output-file: + description: 'Path where the generated CodeQL config file will be written' + required: false + default: '.github/codeql-config-pr.yml' +outputs: + config-file: + description: 'Path to the generated CodeQL config file' + value: ${{ steps.generate.outputs.config-file }} + skip: + description: 'true if no paths were resolved and CodeQL should be skipped' + value: ${{ steps.generate.outputs.skip }} + +runs: + using: composite + steps: + - name: Generate CodeQL config + id: generate + shell: bash + env: + CONFIG_FILE: ${{ inputs.output-file }} + RAW_PATHS: ${{ inputs.changed-paths }} + run: | + # Normalize separators (comma or newline) to newline + PATHS=$(printf '%s' "$RAW_PATHS" | tr ',' '\n' | sed '/^[[:space:]]*$/d' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + if [ -z "$PATHS" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "No paths provided — skipping CodeQL config generation" + exit 0 + fi + + # Remove duplicates and empty lines + PATHS=$(echo "$PATHS" | sort -u | sed '/^[[:space:]]*$/d') + + if [ -z "$PATHS" ]; then + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "No paths resolved — skipping CodeQL config generation" + exit 0 + fi + + # Generate config + { + echo "paths:" + echo "$PATHS" | while IFS= read -r p; do + echo " - '$p'" + done + } > "$CONFIG_FILE" + + echo "skip=false" >> "$GITHUB_OUTPUT" + echo "config-file=$CONFIG_FILE" >> "$GITHUB_OUTPUT" + + count=$(echo "$PATHS" | wc -l | tr -d ' ') + echo "::group::CodeQL config — $count path(s)" + cat "$CONFIG_FILE" + echo "::endgroup::" diff --git a/src/security/codeql-init/README.md b/src/security/codeql-init/README.md new file mode 100644 index 00000000..94ae1605 --- /dev/null +++ b/src/security/codeql-init/README.md @@ -0,0 +1,61 @@ + + + + + +
Lerian

codeql-init

+ +Composite action that wraps [`github/codeql-action/init`](https://github.com/github/codeql-action) to initialize CodeQL analysis for specified languages. Uses the `security-extended` query suite by default for broader coverage. Designed to be paired with [`codeql-analyze`](../codeql-analyze/). + +## Inputs + +| Input | Description | Required | Default | +|---|---|:---:|---| +| `languages` | Languages to analyze (comma-separated, e.g., `actions`, `javascript-typescript`, `go`) | Yes | — | +| `queries` | Query suite to use (`security-standard`, `security-extended`, `security-and-quality`) | No | `security-extended` | +| `config-file` | Path to CodeQL configuration file | No | — | + +## Usage + +### Basic (GitHub Actions YAML analysis) + +```yaml +jobs: + codeql: + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + security-events: write + actions: read + steps: + - uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.x.x + with: + languages: actions + + - name: Analyze + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-analyze@v1.x.x + with: + category: '/language:actions' +``` + +### Multiple languages + +```yaml +- name: Initialize CodeQL + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.x.x + with: + languages: javascript-typescript + queries: security-and-quality +``` + +## Permissions required + +```yaml +permissions: + contents: read + security-events: write + actions: read # required for workflows using private repositories +``` diff --git a/src/security/codeql-init/action.yml b/src/security/codeql-init/action.yml new file mode 100644 index 00000000..a2442047 --- /dev/null +++ b/src/security/codeql-init/action.yml @@ -0,0 +1,25 @@ +name: CodeQL Init +description: Initialize CodeQL analysis for specified languages with configurable query suite + +inputs: + languages: + description: 'Languages to analyze (comma-separated, e.g., actions, javascript-typescript, go)' + required: true + queries: + description: 'Query suite to use (security-standard, security-extended, security-and-quality)' + required: false + default: 'security-extended' + config-file: + description: 'Path to CodeQL configuration file' + required: false + default: '' + +runs: + using: composite + steps: + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ inputs.languages }} + queries: ${{ inputs.queries }} + config-file: ${{ inputs.config-file || '' }} diff --git a/src/security/codeql-reporter/README.md b/src/security/codeql-reporter/README.md new file mode 100644 index 00000000..c0428750 --- /dev/null +++ b/src/security/codeql-reporter/README.md @@ -0,0 +1,60 @@ + + + + + +
Lerian

codeql-reporter

+ +Composite action that reads CodeQL SARIF output and posts a formatted security report as a PR comment. Uses an idempotent comment strategy (updates existing comment on re-runs). Designed to run after [`codeql-analyze`](../codeql-analyze/). + +## Inputs + +| Input | Description | Required | Default | +|---|---|:---:|---| +| `github-token` | GitHub token with `pull-requests:write` and `issues:write` permissions | Yes | — | +| `sarif-path` | Path to the CodeQL SARIF output directory | No | `../results` | +| `languages` | Comma-separated list of languages analyzed (for display) | Yes | — | +| `fail-on-findings` | Fail the step when security findings are detected (`true`/`false`) | No | `false` | + +## Outputs + +| Output | Description | +|---|---| +| `has-findings` | `true` if any CodeQL findings were detected | +| `findings-count` | Total number of CodeQL findings | + +## Usage + +### After CodeQL analysis (paired with codeql-init and codeql-analyze) + +```yaml +steps: + - uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-init@v1.x.x + with: + languages: go + + - name: Perform CodeQL Analysis + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-analyze@v1.x.x + with: + category: '/language:go' + + - name: Post CodeQL Results to PR + if: always() && github.event_name == 'pull_request' + uses: LerianStudio/github-actions-shared-workflows/src/security/codeql-reporter@v1.x.x + with: + github-token: ${{ github.token }} + languages: go +``` + +## Permissions required + +```yaml +permissions: + contents: read + security-events: write + pull-requests: write + actions: read +``` diff --git a/src/security/codeql-reporter/action.yml b/src/security/codeql-reporter/action.yml new file mode 100644 index 00000000..398041db --- /dev/null +++ b/src/security/codeql-reporter/action.yml @@ -0,0 +1,239 @@ +name: CodeQL Reporter +description: Reads CodeQL SARIF output and posts a formatted security report as a PR comment. + +inputs: + github-token: + description: GitHub token with pull-requests:write and issues:write permissions + required: true + sarif-path: + description: Path to the CodeQL SARIF output directory + required: false + default: '../results' + languages: + description: Comma-separated list of languages analyzed (for display purposes) + required: true + fail-on-findings: + description: 'Fail the step with exit code 1 when security findings are detected (true/false)' + required: false + default: 'false' + +outputs: + has-findings: + description: True if any CodeQL findings were detected + value: ${{ steps.parse-outputs.outputs.has_findings }} + findings-count: + description: Total number of CodeQL findings + value: ${{ steps.parse-outputs.outputs.findings_count }} + +runs: + using: composite + steps: + - name: Post CodeQL report to PR + id: report + uses: actions/github-script@v8 + env: + SARIF_PATH: ${{ inputs.sarif-path }} + LANGUAGES: ${{ inputs.languages }} + with: + github-token: ${{ inputs.github-token }} + result-encoding: string + script: | + const fs = require('fs'); + const path = require('path'); + + const sarifDir = process.env.SARIF_PATH; + const languages = process.env.LANGUAGES; + + let body = ''; + let hasFindings = false; + let findingsCount = 0; + let sarifAvailable = true; + let hadParseErrors = false; + + // ── Helpers ── + const SEVERITY_ICONS = { + error: '\u{1F534}', + warning: '\u{1F7E1}', + note: '\u{1F535}', + none: '\u26AA' + }; + + const SEVERITY_LABELS = { + error: 'High/Critical', + warning: 'Medium', + note: 'Low', + none: 'Info' + }; + + const SEVERITY_ORDER = { error: 0, warning: 1, note: 2, none: 3 }; + + const md = (value) => + String(value ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').replace(/`/g, '\\`'); + + const truncate = (str, max) => + str.length > max ? str.substring(0, max - 3) + '...' : str; + + // ── Read SARIF files ── + function readSarifFiles() { + const findings = []; + + if (!fs.existsSync(sarifDir)) { + sarifAvailable = false; + core.warning(`SARIF directory not found: ${sarifDir}`); + return findings; + } + + const files = fs.readdirSync(sarifDir).filter(f => f.endsWith('.sarif')); + + if (files.length === 0) { + sarifAvailable = false; + core.warning('No SARIF files found in output directory'); + return findings; + } + + for (const file of files) { + try { + const sarif = JSON.parse(fs.readFileSync(path.join(sarifDir, file), 'utf8')); + + for (const run of (sarif.runs || [])) { + const rules = new Map(); + for (const rule of (run.tool?.driver?.rules || [])) { + rules.set(rule.id, rule); + } + + for (const result of (run.results || [])) { + const rule = rules.get(result.ruleId) || {}; + const location = result.locations?.[0]?.physicalLocation; + + findings.push({ + rule: result.ruleId || 'unknown', + severity: result.level || 'warning', + message: result.message?.text || rule.shortDescription?.text || 'No description', + file: location?.artifactLocation?.uri || 'unknown', + line: location?.region?.startLine || 0, + description: rule.fullDescription?.text || rule.shortDescription?.text || '', + tags: rule.properties?.tags || [], + }); + } + } + } catch (e) { + hadParseErrors = true; + core.warning(`Failed to parse SARIF file ${file}: ${e.message}`); + } + } + + return findings; + } + + // ── Build Report ── + const findings = readSarifFiles(); + findingsCount = findings.length; + + body += `## \u{1F6E1}\uFE0F CodeQL Analysis Results\n\n`; + body += `**Languages analyzed:** \`${languages}\`\n\n`; + + if (!sarifAvailable) { + body += `\u26A0\uFE0F CodeQL did not produce readable SARIF output. Check the analysis step logs.\n\n`; + } else if (hadParseErrors && findings.length === 0) { + body += `\u26A0\uFE0F Some SARIF files could not be parsed and no findings were recovered. Check the analysis step logs.\n\n`; + } else if (findings.length === 0) { + body += `\u2705 No security issues found.\n\n`; + } else { + hasFindings = true; + + if (hadParseErrors) { + body += `\u26A0\uFE0F Some SARIF files could not be parsed. Findings below may be incomplete.\n\n`; + } + + findings.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3)); + + // Summary counts + const counts = {}; + for (const f of findings) { + const label = SEVERITY_LABELS[f.severity] || 'Info'; + counts[label] = (counts[label] || 0) + 1; + } + + body += `**Found ${findings.length} issue(s):** `; + body += Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', '); + body += `\n\n`; + + // Findings table + const MAX = 30; + body += `| Severity | Rule | File | Message |\n`; + body += `|----------|------|------|----------|\n`; + + for (const f of findings.slice(0, MAX)) { + const icon = SEVERITY_ICONS[f.severity] || '\u26AA'; + const label = SEVERITY_LABELS[f.severity] || 'Info'; + const fileLink = f.line > 0 ? `\`${md(f.file)}:${f.line}\`` : `\`${md(f.file)}\``; + body += `| ${icon} ${label} | \`${md(f.rule)}\` | ${fileLink} | ${md(truncate(f.message, 80))} |\n`; + } + + if (findings.length > MAX) { + body += `\n_... and ${findings.length - MAX} more findings._\n`; + } + + body += '\n'; + } + + // ── Useful Links ── + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const securityUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/security/code-scanning?query=pr%3A${context.issue.number}+is%3Aopen`; + body += `---\n\n`; + body += `\u{1F50D} [View full scan logs](${runUrl}) | \u{1F6E1}\uFE0F [Security tab](${securityUrl})\n\n`; + + // ── Post Comment ── + const marker = ``; + body = marker + '\n' + body; + + try { + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + + const existing = comments.find(c => c.body?.includes(marker)); + const params = { + owner: context.repo.owner, + repo: context.repo.repo, + body, + }; + + if (existing) { + await github.rest.issues.updateComment({ ...params, comment_id: existing.id }); + } else { + await github.rest.issues.createComment({ ...params, issue_number: context.issue.number }); + } + } catch (e) { + core.warning(`Could not post PR CodeQL comment: ${e.message}`); + } + + return JSON.stringify({ has_findings: hasFindings, findings_count: findingsCount }); + + - name: Parse outputs + id: parse-outputs + shell: bash + env: + RESULT: ${{ steps.report.outputs.result }} + run: | + HAS_FINDINGS=$(echo "$RESULT" | jq -r '.has_findings // false') + FINDINGS_COUNT=$(echo "$RESULT" | jq -r '.findings_count // 0') + echo "has_findings=$HAS_FINDINGS" >> "$GITHUB_OUTPUT" + echo "findings_count=$FINDINGS_COUNT" >> "$GITHUB_OUTPUT" + echo "Parsed outputs: has_findings=$HAS_FINDINGS, findings_count=$FINDINGS_COUNT" + + # ----------------- Security Gate ----------------- + - name: Gate - Fail on CodeQL Findings + if: inputs.fail-on-findings == 'true' + shell: bash + env: + HAS_FINDINGS: ${{ steps.parse-outputs.outputs.has_findings }} + FINDINGS_COUNT: ${{ steps.parse-outputs.outputs.findings_count }} + run: | + if [ "$HAS_FINDINGS" = "true" ]; then + echo "::error::CodeQL found $FINDINGS_COUNT security issue(s). Check the PR comment for details." + exit 1 + fi diff --git a/src/security/dockerfile-checks/README.md b/src/security/dockerfile-checks/README.md index 283054e4..636fc873 100644 --- a/src/security/dockerfile-checks/README.md +++ b/src/security/dockerfile-checks/README.md @@ -41,7 +41,7 @@ jobs: - name: Dockerfile Compliance Checks id: dockerfile-checks - uses: LerianStudio/github-actions-shared-workflows/src/security/dockerfile-checks@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/dockerfile-checks@v1.x.x with: dockerfile-path: './Dockerfile' diff --git a/src/security/pr-security-reporter/README.md b/src/security/pr-security-reporter/README.md index 197fe89f..70935c0d 100644 --- a/src/security/pr-security-reporter/README.md +++ b/src/security/pr-security-reporter/README.md @@ -43,7 +43,7 @@ This composite expects the following files in the runner working directory, gene - name: Post Security Scan Results to PR id: post-results if: always() && github.event_name == 'pull_request' - uses: LerianStudio/github-actions-shared-workflows/src/security/pr-security-reporter@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/pr-security-reporter@v1.x.x with: github-token: ${{ secrets.GITHUB_TOKEN }} app-name: ${{ env.APP_NAME }} diff --git a/src/security/trivy-fs-scan/README.md b/src/security/trivy-fs-scan/README.md index 1a223366..8e0568a5 100644 --- a/src/security/trivy-fs-scan/README.md +++ b/src/security/trivy-fs-scan/README.md @@ -46,7 +46,7 @@ jobs: - name: Trivy Filesystem Scan id: fs-scan - uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@v1.x.x with: scan-ref: '.' app-name: 'my-service' @@ -56,7 +56,7 @@ jobs: ```yaml - name: Trivy Filesystem Scan - uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-fs-scan@v1.x.x with: scan-ref: ${{ matrix.working_dir }} app-name: ${{ matrix.name }} diff --git a/src/security/trivy-image-scan/README.md b/src/security/trivy-image-scan/README.md index 0d1b4a3e..3522bb14 100644 --- a/src/security/trivy-image-scan/README.md +++ b/src/security/trivy-image-scan/README.md @@ -56,7 +56,7 @@ jobs: tags: myorg/myapp:scan - name: Trivy Image Scan - uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-image-scan@develop + uses: LerianStudio/github-actions-shared-workflows/src/security/trivy-image-scan@v1.x.x with: image-ref: 'myorg/myapp:scan' app-name: 'my-service'