diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b663a6c..369296f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,6 +3,10 @@ version: 2 updates: - package-ecosystem: github-actions directory: / + groups: + codeql: + patterns: + - "github/codeql-action/*" commit-message: prefix: "chore(deps)" schedule: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..4840d7b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,40 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [actions, javascript-typescript] + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 + with: + build-mode: none + languages: ${{ matrix.language }} + queries: security-extended + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 7844a3b..88851ba 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,22 +1,76 @@ -name: Dependabot auto-merge +name: Dependabot strict gate on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [main] permissions: {} jobs: - dependabot: - name: Enable auto-merge - if: github.event.pull_request.user.login == 'dependabot[bot]' + strict-gate: + name: Strict Dependabot gate runs-on: ubuntu-24.04 - timeout-minutes: 5 + timeout-minutes: 30 permissions: - contents: write - pull-requests: write + checks: read + contents: read + pull-requests: read + env: + GH_TOKEN: ${{ github.token }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} steps: - - name: Enable auto-merge - env: - GH_TOKEN: ${{ github.token }} - PR_URL: ${{ github.event.pull_request.html_url }} - run: gh pr merge --auto --squash "$PR_URL" + - name: Require successful checks on the exact Dependabot head + run: | + set -euo pipefail + if [[ "$GITHUB_EVENT_NAME" != pull_request || "$PR_AUTHOR" != 'dependabot[bot]' ]]; then + echo 'No Dependabot pull request to gate.' + exit 0 + fi + + for attempt in $(seq 1 50); do + current_head=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.sha) + if [[ "$current_head" != "$PR_HEAD_SHA" ]]; then + echo 'Pull request head changed; the new run must establish its own gate.' >&2 + exit 1 + fi + + checks=$(gh api "repos/$GITHUB_REPOSITORY/commits/$PR_HEAD_SHA/check-runs?per_page=100") + if jq -e ' + .check_runs + | map({key: [.name, .app.id], run: .}) + | group_by(.key) + | map(max_by([.run.started_at, .run.id]) | .run) as $latest + | ([ + ["CI", 15368], + ["Check", 15368], + ["Node 20 engine floor", 15368], + ["CodeQL", 57789], + ["Analyze (actions)", 15368], + ["Analyze (javascript-typescript)", 15368] + ] | all(.[]; . as $required | + ($latest | any(.[]; + .name == $required[0] and .app.id == $required[1] and + .status == "completed" and .conclusion == "success" + )) + )) + and ($latest | all(.[]; + .name == "Strict Dependabot gate" or + .name == "Enable auto-merge" or + (.name == "deploy" and .status == "completed" and .conclusion == "skipped") or + (.status == "completed" and .conclusion == "success") + )) + ' <<< "$checks" > /dev/null; then + echo "All exact-head checks passed on $PR_HEAD_SHA." + exit 0 + fi + + echo "Strict checks are not yet all successful (attempt $attempt/50)." + sleep 30 + done + + echo "Strict checks did not all pass on $PR_HEAD_SHA." >&2 + exit 1