diff --git a/.github/labels.yml b/.github/labels.yml index 80ad9adf..c772f2ba 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -30,6 +30,10 @@ color: "ededed" description: Dependency updates (usually opened by Dependabot) +- name: github-actions + color: "2088FF" + description: Updates to GitHub Actions dependencies (Dependabot ecosystem) + - name: github-config color: "f9d0c4" description: Changes to repository configuration (templates, CODEOWNERS, labeler, etc.) diff --git a/.github/workflows/go-release.yml b/.github/workflows/go-release.yml index b7c2d76c..8e9311af 100644 --- a/.github/workflows/go-release.yml +++ b/.github/workflows/go-release.yml @@ -101,7 +101,7 @@ jobs: run: ${{ inputs.test_cmd }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@e24998b8b67b290c2fa8b7c14fcfa7de2c5c9b8c # v7 + uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7 with: distribution: ${{ inputs.goreleaser_distribution }} version: ${{ inputs.goreleaser_version }} diff --git a/.github/workflows/go-security.yml b/.github/workflows/go-security.yml index 79288dce..311ea1f3 100644 --- a/.github/workflows/go-security.yml +++ b/.github/workflows/go-security.yml @@ -76,10 +76,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Dependency Review - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 with: fail-on-severity: moderate @@ -90,22 +90,22 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ inputs.go_version }} cache: true - name: Run Gosec Security Scanner - uses: securego/gosec@master + uses: securego/gosec@223e19b8856e00f02cc67804499a83f77e208f3c # v2.25.0 with: args: '-no-fail -fmt sarif -out gosec-results.sarif ./...' - name: Upload Gosec SARIF if: always() && inputs.upload_sarif - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 with: sarif_file: gosec-results.sarif category: gosec @@ -117,10 +117,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ inputs.go_version }} cache: true @@ -138,10 +138,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ inputs.go_version }} cache: true @@ -150,7 +150,7 @@ jobs: run: go list -json -m all > go.list - name: Nancy vulnerability scan - uses: sonatype-nexus-community/nancy-github-action@main + uses: sonatype-nexus-community/nancy-github-action@726e338312e68ecdd4b4195765f174d3b3ce1533 # v1.0.3 with: nancyCommand: sleuth --loud continue-on-error: true @@ -162,10 +162,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.35.0 + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: scan-type: 'fs' scan-ref: '.' @@ -175,7 +175,7 @@ jobs: - name: Upload Trivy SARIF if: always() && inputs.upload_sarif - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 with: sarif_file: trivy-results.sarif category: trivy @@ -187,12 +187,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@main + uses: trufflesecurity/trufflehog@17456f8c7d042d8c82c9a8ca9e937231f9f42e26 # v3.95.2 with: path: ./ base: ${{ github.event.repository.default_branch }} @@ -206,10 +206,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ inputs.go_version }} cache: true @@ -227,7 +227,7 @@ jobs: cat licenses.txt - name: Upload license report - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: license-report path: licenses.txt @@ -240,22 +240,22 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Go - uses: actions/setup-go@v6 + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 with: go-version: ${{ inputs.go_version }} cache: true - name: Generate SBOM - uses: anchore/sbom-action@v0 + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: format: spdx-json output-file: sbom.spdx.json - name: Upload SBOM - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sbom path: sbom.spdx.json diff --git a/.github/workflows/self-release.yml b/.github/workflows/self-release.yml index 0e32d939..9417c49c 100644 --- a/.github/workflows/self-release.yml +++ b/.github/workflows/self-release.yml @@ -17,6 +17,7 @@ on: # The deployment matrix is resolved from main at runtime by callers, so # matrix-only changes propagate without a new release tag. - 'config/deployment-matrix.yml' + - '.github/workflows/self-*.yml' tags-ignore: - '**' diff --git a/.github/workflows/self-routine.yml b/.github/workflows/self-routine.yml index 284713ac..26389dc1 100644 --- a/.github/workflows/self-routine.yml +++ b/.github/workflows/self-routine.yml @@ -2,11 +2,12 @@ name: Self — Repository Routines on: schedule: - - cron: "0 3 * * 1" # weekly Mon 03:00 UTC — branch cleanup (stale scan) - - cron: "0 4 * * *" # daily 04:00 UTC — stale PRs - - cron: "30 4 * * 3" # weekly Wed 04:30 UTC — stale issues - - cron: "0 5 * * 0" # weekly Sun 05:00 UTC — labels sync (reconciliation) - - cron: "0 6 1 * *" # monthly 1st 06:00 UTC — workflow runs cleanup + # Single weekly cron — every Monday at 03:00 UTC fires every routine in + # the same workflow run. Stale/cleanup actions are idempotent: any item + # that doesn't meet the threshold is logged and skipped, so running them + # together once a week is the simplest mental model with no real cost + # over staggered cadences. + - cron: "0 3 * * 1" pull_request: types: [closed] push: @@ -52,7 +53,7 @@ jobs: branch_cleanup_stale: name: Clean stale branches if: | - (github.event_name == 'schedule' && github.event.schedule == '0 3 * * 1') + github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (inputs.routine == 'all' || inputs.routine == 'branch-cleanup-stale')) uses: ./.github/workflows/branch-cleanup.yml with: @@ -65,10 +66,11 @@ jobs: stale_pr: name: Close stale PRs if: | - (github.event_name == 'schedule' && github.event.schedule == '0 4 * * *') + github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (inputs.routine == 'all' || inputs.routine == 'stale-pr')) uses: ./.github/workflows/stale-pr.yml with: + operations_per_run: ${{ github.event_name == 'schedule' && 420 || 60 }} dry_run: ${{ inputs.dry_run || false }} secrets: inherit @@ -77,7 +79,7 @@ jobs: stale_issue: name: Close stale issues if: | - (github.event_name == 'schedule' && github.event.schedule == '30 4 * * 3') + github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (inputs.routine == 'all' || inputs.routine == 'stale-issue')) uses: ./.github/workflows/stale-issue.yml with: @@ -92,7 +94,7 @@ jobs: name: Sync labels if: | github.event_name == 'push' - || (github.event_name == 'schedule' && github.event.schedule == '0 5 * * 0') + || github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (inputs.routine == 'all' || inputs.routine == 'labels-sync')) uses: ./.github/workflows/labels-sync.yml with: @@ -104,7 +106,7 @@ jobs: workflow_runs_cleanup: name: Delete old workflow runs if: | - (github.event_name == 'schedule' && github.event.schedule == '0 6 1 * *') + github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (inputs.routine == 'all' || inputs.routine == 'workflow-runs-cleanup')) uses: ./.github/workflows/workflow-runs-cleanup.yml with: diff --git a/src/config/stale/action.yml b/src/config/stale/action.yml index cc4149bb..bb60fa9f 100644 --- a/src/config/stale/action.yml +++ b/src/config/stale/action.yml @@ -100,19 +100,13 @@ runs: echo "::notice title=Dry run::No labels or comments will be applied (debug-only mode)" fi - cat <<'NOTE' - - Reading the per-item log below: - • actions/stale uses the unified /issues API, so every item is - announced as "Issue #N" — even pull requests. - • Internally the tool checks isPullRequest and routes each item - to the matching rule set (days-before-pr-* vs days-before-issue-*). - • Items of a type whose days-before-*-stale is -1 are listed but - never marked, commented on, or closed. - - NOTE + # Open a collapsible group around the verbose actions/stale per-item + # output that follows. The group is closed in the post-run summary + # step. Readers see the noisy log collapsed by default in the UI. + echo "::group::actions/stale per-item log (verbose; expand to inspect)" - name: Flag and close stale PRs and issues + id: stale uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: repo-token: ${{ inputs.github-token }} @@ -139,3 +133,41 @@ runs: Closing this PR due to prolonged inactivity. Reopen if work resumes. close-issue-message: >- Closing this issue due to prolonged inactivity. Reopen if it is still relevant. + + # ----------------- Post-run Summary ----------------- + # Closes the collapsible group opened in the pre-flight step and emits + # a clean, deduplicated summary derived from the action's outputs so + # readers don't have to scan the verbose log to know what happened. + - name: Summarize stale scan outcome + if: always() + shell: bash + env: + STALE_OUTCOME: ${{ steps.stale.outcome }} + STALED: ${{ steps.stale.outputs.staled-issues-prs }} + CLOSED: ${{ steps.stale.outputs.closed-issues-prs }} + run: | + echo "::endgroup::" + echo "" + + # When actions/stale didn't succeed, its outputs are empty for the + # wrong reason — reporting 0 marked / 0 closed would lie about the + # state of the run. Surface the real outcome instead. + if [ "$STALE_OUTCOME" != "success" ]; then + echo "::warning title=Stale scan summary unavailable::actions/stale outcome=${STALE_OUTCOME}; counts are not reliable." + exit 0 + fi + + count_csv() { + local csv="$1" + [ -z "$csv" ] && { echo 0; return; } + # Items are comma-separated; awk avoids subshell + tr cost. + awk -v s="$csv" 'BEGIN { n = split(s, a, ","); print n }' + } + + staled_count=$(count_csv "$STALED") + closed_count=$(count_csv "$CLOSED") + + echo "::notice title=Stale scan summary::Marked stale: ${staled_count} | Closed: ${closed_count}" + [ -n "$STALED" ] && echo " Marked stale: ${STALED}" + [ -n "$CLOSED" ] && echo " Closed: ${CLOSED}" + echo ""