diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5ced200 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: 2006-2026 Knut Reinert & Freie Universität Berlin +# SPDX-FileCopyrightText: 2016-2026 Knut Reinert & MPI für molekulare Genetik +# SPDX-License-Identifier: CC0-1.0 + +# Staged CI: cheap checks -> Linux unit tests -> everything else. A later stage only starts if the earlier ones passed, +# so a formatting error costs one short Lint job instead of a full CI run. +# +# Stage 1 Lint (ci_lint.yml), License (ci_license.yml) +# Stage 2 Linux (ci_linux.yml) +# Stage 3 Coverage, Install, macOS +# +# Runs PR code, therefore only `pull_request`: read-only GITHUB_TOKEN, no secrets for fork PRs. If Lint fails, +# lint_autofix.yml (from the default branch) pushes the formatting fix, which starts CI again. +# The only required status check is "CI result" (plus "Documentation", which stays a separate workflow). + +name: CI + +on: + push: + branches: + - 'main' + - 'hibf' + pull_request: + workflow_dispatch: + +# Replaces the cancel_running_workflows job of the old lint workflow: a new push to a PR cancels all stages. +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'push' }} + +permissions: + contents: read + +jobs: + # Stage 1. As in all of the repository's workflows: on forks, only when started by hand. + lint: + name: Lint + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' + uses: ./.github/workflows/ci_lint.yml + license: + name: License + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' + uses: ./.github/workflows/ci_license.yml + + # Stage 2 + linux: + name: Linux + needs: [lint, license] + uses: ./.github/workflows/ci_linux.yml + + # Stage 3. Coverage gets the Codecov token; the other stages need no secrets. + coverage: + name: Coverage + needs: linux + uses: ./.github/workflows/ci_coverage.yml + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + install: + name: Install + needs: linux + uses: ./.github/workflows/ci_install.yml + macos: + name: macOS + needs: linux + uses: ./.github/workflows/ci_macos.yml + + # The single required check. Required checks cannot simply list the stages: a skipped job reports "Success", and a + # skipped matrix job reports under its unexpanded name. This gate runs always and fails unless every stage + # succeeded (skipped and cancelled count as failure). + result: + name: CI result + if: always() && (github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch') + needs: [lint, license, linux, coverage, install, macos] + runs-on: ubuntu-latest + steps: + - name: Check stage results + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + run: | + for result in ${RESULTS}; do + if [[ ${result} != success ]]; then + echo "::error::Not every CI stage succeeded: ${RESULTS}" + exit 1 + fi + done diff --git a/.github/workflows/ci_coverage.yml b/.github/workflows/ci_coverage.yml index 7a0eb2a..efb8112 100644 --- a/.github/workflows/ci_coverage.yml +++ b/.github/workflows/ci_coverage.yml @@ -5,19 +5,12 @@ name: Coverage on: - push: - branches: - - 'main' - - 'hibf' - pull_request: - types: - - unlabeled + workflow_call: + secrets: + CODECOV_TOKEN: + required: false workflow_dispatch: -concurrency: - group: coverage-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} - env: SHARG_NO_VERSION_CHECK: 1 TZ: Europe/Berlin @@ -30,7 +23,7 @@ jobs: build: runs-on: ubuntu-latest name: ${{ matrix.compiler }} - if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' || github.event.label.name == 'lint' + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' strategy: fail-fast: false matrix: @@ -39,7 +32,7 @@ jobs: image: ghcr.io/seqan/${{ matrix.compiler }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -79,7 +72,7 @@ jobs: --output ${GITHUB_WORKSPACE}/build/coverage_report.xml - name: Submit coverage build - uses: codecov/codecov-action@v6.0.0 + uses: codecov/codecov-action@v7.1.1 with: disable_search: true fail_ci_if_error: false diff --git a/.github/workflows/ci_documentation.yml b/.github/workflows/ci_documentation.yml index 29a67ea..17d9303 100644 --- a/.github/workflows/ci_documentation.yml +++ b/.github/workflows/ci_documentation.yml @@ -1,7 +1,11 @@ -# SPDX-FileCopyrightText: 2006-2025 Knut Reinert & Freie Universität Berlin -# SPDX-FileCopyrightText: 2016-2025 Knut Reinert & MPI für molekulare Genetik +# SPDX-FileCopyrightText: 2006-2026 Knut Reinert & Freie Universität Berlin +# SPDX-FileCopyrightText: 2016-2026 Knut Reinert & MPI für molekulare Genetik # SPDX-License-Identifier: CC0-1.0 +# Builds and tests the documentation. PRs: upload the HTML; docs_preview.yml publishes it from the default branch. +# Push to main and hibf: deploy to docs.seqan.de from a separate job, so the server key never shares a job with +# CMake/Doxygen. Stays a separate workflow (not a CI stage) so that the preview appears as soon as this finishes. + name: Documentation on: @@ -9,16 +13,16 @@ on: branches: - 'main' - 'hibf' - pull_request_target: - types: - - unlabeled - - closed + pull_request: workflow_dispatch: concurrency: group: documentation-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name != 'push' }} +permissions: + contents: read + env: SHARG_NO_VERSION_CHECK: 1 TZ: Europe/Berlin @@ -31,18 +35,12 @@ jobs: build: name: Documentation runs-on: ubuntu-latest - if: ( github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' || github.event.label.name == 'lint' ) && github.event.action != 'closed' + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' steps: - name: Checkout - if: github.event_name != 'pull_request_target' - uses: actions/checkout@v6 - - # pull_request_target does not checkout the merge commit by default - - name: Checkout - if: github.event_name == 'pull_request_target' - uses: actions/checkout@v6 + uses: actions/checkout@v7 # locked via actions.lock with: - ref: "refs/pull/${{ github.event.number }}/merge" + persist-credentials: false - name: Install Doxygen uses: seqan/actions/setup-doxygen@main @@ -60,35 +58,35 @@ jobs: working-directory: build run: make doc - - name: Deploy Preview - if: github.event_name == 'pull_request_target' - uses: seqan/actions/documentation_deploy_preview@main + # For docs_preview.yml (PRs) and the deploy job below. docs_preview.yml expects these artifact names. + - name: Upload documentation + uses: actions/upload-artifact@v7 # locked via actions.lock with: - deploy_host: ${{ secrets.DEPLOY_HOST }} - deploy_user: ${{ secrets.DEPLOY_USER }} - deploy_ssh_key: ${{ secrets.DEPLOY_SSH_KEY }} - deploy_base_path: ${{ secrets.DEPLOY_PREVIEW_BASE_PATH }} - source_path_user_doc: build/html - token: ${{ secrets.SEQAN_ACTIONS_PAT }} + name: documentation + path: build/html/ + retention-days: 7 + if-no-files-found: error - - name: Deploy Documentation - if: github.event_name == 'push' - uses: seqan/actions/documentation_deploy_production@main + deploy: + name: Deploy Documentation + needs: build + if: github.repository_owner == 'seqan' && github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/hibf') + runs-on: ubuntu-latest + environment: server-deploy # deployment branches: main, hibf; holds the server key + steps: + - name: Download documentation + uses: actions/download-artifact@v8 # locked via actions.lock with: - deploy_host: ${{ secrets.DEPLOY_HOST }} - deploy_user: ${{ secrets.DEPLOY_USER }} - deploy_ssh_key: ${{ secrets.DEPLOY_SSH_KEY }} - source_path_user_doc: build/html/ - deploy_path_user_doc: ${{ secrets.DEPLOY_PRODUCTION_BASE_PATH }}/${{ github.ref_name}} - delete: - name: Delete Preview - runs-on: ubuntu-latest - if: github.repository_owner == 'seqan' && github.event_name == 'pull_request_target' && github.event.action == 'closed' - steps: - - name: Delete Preview - uses: seqan/actions/documentation_delete_preview@main + name: documentation + path: html + + # Plain rsync with a pinned host key (no third-party deploy action): the key grants the whole server. + # Same targets as before. + - name: Deploy + uses: seqan/actions/documentation_deploy_production@main with: deploy_host: ${{ secrets.DEPLOY_HOST }} deploy_user: ${{ secrets.DEPLOY_USER }} deploy_ssh_key: ${{ secrets.DEPLOY_SSH_KEY }} - deploy_base_path: ${{ secrets.DEPLOY_PREVIEW_BASE_PATH }} + source_path_user_doc: html/ + deploy_path_user_doc: ${{ secrets.DEPLOY_PRODUCTION_BASE_PATH }}/${{ github.ref_name }} diff --git a/.github/workflows/ci_install.yml b/.github/workflows/ci_install.yml index e1111ff..d9d3796 100644 --- a/.github/workflows/ci_install.yml +++ b/.github/workflows/ci_install.yml @@ -5,19 +5,9 @@ name: Install on: - push: - branches: - - 'main' - - 'hibf' - pull_request: - types: - - unlabeled + workflow_call: workflow_dispatch: -concurrency: - group: install-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} - env: SHARG_NO_VERSION_CHECK: 1 TZ: Europe/Berlin @@ -30,7 +20,7 @@ jobs: build: runs-on: ubuntu-latest name: ${{ matrix.compiler }} - if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' || github.event.label.name == 'lint' + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' strategy: fail-fast: false matrix: @@ -40,7 +30,7 @@ jobs: options: --privileged steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: path: needle diff --git a/.github/workflows/ci_license.yml b/.github/workflows/ci_license.yml index 6443a1d..a86d2c8 100644 --- a/.github/workflows/ci_license.yml +++ b/.github/workflows/ci_license.yml @@ -5,19 +5,9 @@ name: License on: - push: - branches: - - 'main' - - 'hibf' - pull_request: - types: - - unlabeled + workflow_call: workflow_dispatch: -concurrency: - group: license-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} - env: TZ: Europe/Berlin @@ -25,10 +15,10 @@ jobs: check: name: REUSE Compliance runs-on: ubuntu-latest - if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' || github.event.label.name == 'lint' + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: REUSE Compliance Check uses: fsfe/reuse-action@v6 diff --git a/.github/workflows/ci_lint.yml b/.github/workflows/ci_lint.yml index 5b26693..36d3ba7 100644 --- a/.github/workflows/ci_lint.yml +++ b/.github/workflows/ci_lint.yml @@ -1,48 +1,45 @@ -# SPDX-FileCopyrightText: 2006-2025 Knut Reinert & Freie Universität Berlin -# SPDX-FileCopyrightText: 2016-2025 Knut Reinert & MPI für molekulare Genetik +# SPDX-FileCopyrightText: 2006-2026 Knut Reinert & Freie Universität Berlin +# SPDX-FileCopyrightText: 2016-2026 Knut Reinert & MPI für molekulare Genetik # SPDX-License-Identifier: CC0-1.0 +# Stage 1 of ci.yml. Check only: formats the checkout with seqan/actions/lint and fails if that changed anything. +# The CI run then ends early (later stages are skipped), and lint_autofix.yml pushes the formatting commit, which +# starts a new CI run. The diff is shown in the job summary, for PRs the bot cannot push to. + name: Lint on: - pull_request_target: + workflow_call: -env: - TZ: Europe/Berlin +permissions: + contents: read defaults: run: shell: bash -Eeuxo pipefail {0} jobs: - # Cancel other workflows that are dependent on this workflow by adding jobs that have the same concurrency group. - cancel_running_workflows: - name: Cancel ${{ matrix.workflow}} - strategy: - fail-fast: false - matrix: - workflow: ["linux", "macos", "coverage", "install", "documentation"] - concurrency: - group: ${{ matrix.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - runs-on: ubuntu-latest - steps: - - name: "Cancel ${{ matrix.workflow }}" - run: echo "Cancelling ${{ matrix.workflow }}" lint: name: Lint - concurrency: - group: lint-${{ github.event.pull_request.number }} - cancel-in-progress: true - needs: cancel_running_workflows runs-on: ubuntu-latest timeout-minutes: 15 steps: - - name: Run lint + - name: Checkout + uses: actions/checkout@v7 # locked via actions.lock + with: + fetch-depth: 0 + persist-credentials: false + + # Files changed since: the PR's base (pull_request), the previous tip (push), or the last commit (dispatch). + - name: Format uses: seqan/actions/lint@main with: - clang_format: 18 - token: ${{ secrets.SEQAN_ACTIONS_PAT }} - gpg_key: ${{ secrets.SEQAN_ACTIONS_GPG_KEY }} - gpg_passphrase: ${{ secrets.SEQAN_ACTIONS_GPG_PASSPHRASE }} + base: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD~1' }} + - name: Check + run: | + if ! git diff --quiet; then + { echo '### Formatting required'; echo '```diff'; git diff; echo '```'; } >> "${GITHUB_STEP_SUMMARY}" + echo "::error::Formatting required. The bot pushes a fix if the PR allows edits by maintainers." + exit 1 + fi diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index be698fb..abeaa1d 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -5,19 +5,9 @@ name: Linux on: - push: - branches: - - 'main' - - 'hibf' - pull_request: - types: - - unlabeled + workflow_call: workflow_dispatch: -concurrency: - group: linux-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} - env: SHARG_NO_VERSION_CHECK: 1 TZ: Europe/Berlin @@ -30,7 +20,7 @@ jobs: build: runs-on: ubuntu-latest name: ${{ matrix.compiler }} - if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' || github.event.label.name == 'lint' + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' strategy: fail-fast: false matrix: @@ -49,7 +39,7 @@ jobs: fi - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup cache uses: seqan/actions/setup-actions-cache@main diff --git a/.github/workflows/ci_macos.yml b/.github/workflows/ci_macos.yml index 826cd72..e4b9eb9 100644 --- a/.github/workflows/ci_macos.yml +++ b/.github/workflows/ci_macos.yml @@ -5,19 +5,9 @@ name: macOS on: - push: - branches: - - 'main' - - 'hibf' - pull_request: - types: - - unlabeled + workflow_call: workflow_dispatch: -concurrency: - group: macos-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} - env: SHARG_NO_VERSION_CHECK: 1 TZ: Europe/Berlin @@ -30,14 +20,14 @@ jobs: build: name: ${{ matrix.compiler }} runs-on: macos-latest - if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' || github.event.label.name == 'lint' + if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' strategy: fail-fast: false matrix: compiler: ["clang-latest", "clang-second-latest", "clang-third-latest", "gcc-latest", "gcc-second-latest"] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup toolchain uses: seqan/actions/setup-toolchain@main diff --git a/.github/workflows/cron_codechecker.yml b/.github/workflows/cron_codechecker.yml index 807b048..ac1c3b1 100644 --- a/.github/workflows/cron_codechecker.yml +++ b/.github/workflows/cron_codechecker.yml @@ -24,6 +24,7 @@ jobs: build: name: CodeChecker runs-on: ubuntu-latest + environment: server-deploy # deployment branch: main if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' container: image: ghcr.io/seqan/codechecker diff --git a/.github/workflows/cron_codeql.yml b/.github/workflows/cron_codeql.yml index 3b982e9..4ebc9b2 100644 --- a/.github/workflows/cron_codeql.yml +++ b/.github/workflows/cron_codeql.yml @@ -32,7 +32,7 @@ jobs: image: ghcr.io/seqan/gcc-second-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/cron_cpm.yml b/.github/workflows/cron_cpm.yml index 81053db..c99002d 100644 --- a/.github/workflows/cron_cpm.yml +++ b/.github/workflows/cron_cpm.yml @@ -20,13 +20,23 @@ jobs: update_cpm: name: Update CPM runs-on: ubuntu-latest + environment: pr-bot # deployment branch: main timeout-minutes: 15 if: github.repository_owner == 'seqan' || github.event_name == 'workflow_dispatch' steps: + - name: Create App token + id: token + uses: actions/create-github-app-token@v3 # locked via actions.lock + with: + client-id: ${{ vars.SEQAN_PR_BOT_CLIENT_ID }} + private-key: ${{ secrets.SEQAN_PR_BOT_KEY }} + permission-contents: write + permission-pull-requests: write + - name: Run update CPM uses: seqan/actions/update_cpm_package_lock@main with: - token: ${{ secrets.SEQAN_ACTIONS_PAT }} + token: ${{ steps.token.outputs.token }} gpg_key: ${{ secrets.SEQAN_ACTIONS_GPG_KEY }} gpg_passphrase: ${{ secrets.SEQAN_ACTIONS_GPG_PASSPHRASE }} package_lock_file: cmake/package-lock.cmake diff --git a/.github/workflows/cron_sanitizer.yml b/.github/workflows/cron_sanitizer.yml index 2ddc7a7..38b024b 100644 --- a/.github/workflows/cron_sanitizer.yml +++ b/.github/workflows/cron_sanitizer.yml @@ -74,7 +74,7 @@ jobs: image: ${{ matrix.image || '' }} steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup compiler if: contains(matrix.os, 'macos') diff --git a/.github/workflows/docs_preview.yml b/.github/workflows/docs_preview.yml new file mode 100644 index 0000000..dbc801f --- /dev/null +++ b/.github/workflows/docs_preview.yml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2006-2026 Knut Reinert & Freie Universität Berlin +# SPDX-FileCopyrightText: 2016-2026 Knut Reinert & MPI für molekulare Genetik +# SPDX-License-Identifier: CC0-1.0 + +# Publishes documentation previews to https://seqan.github.io/docs-preview/seqan/needle// and deletes them when the +# PR is closed. Details: seqan/actions/.github/workflows/docs_preview.yml. + +name: Documentation Preview + +on: + workflow_run: # zizmor: ignore[dangerous-triggers] -- privileged half; the called workflow treats PR data as data only + workflows: [Documentation] + types: [completed] + pull_request_target: # zizmor: ignore[dangerous-triggers] -- no checkout; uses the repository name and PR number only + types: [closed] + +permissions: {} + +jobs: + preview: + name: Documentation Preview + uses: seqan/actions/.github/workflows/docs_preview.yml@main + permissions: + actions: read + pull-requests: read + # Only this secret. This job has no environment, but GitHub resolves it with the environment of the called jobs + # (`docs-preview`). + secrets: + SEQAN_PR_BOT_KEY: ${{ secrets.SEQAN_PR_BOT_KEY }} diff --git a/.github/workflows/lint_autofix.yml b/.github/workflows/lint_autofix.yml new file mode 100644 index 0000000..5a997dc --- /dev/null +++ b/.github/workflows/lint_autofix.yml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2006-2026 Knut Reinert & Freie Universität Berlin +# SPDX-FileCopyrightText: 2016-2026 Knut Reinert & MPI für molekulare Genetik +# SPDX-License-Identifier: CC0-1.0 + +# Pushes a formatting commit to a PR whose CI failed in Lint. Runs from the default branch; the called workflow only +# reads the PR as data. Details: seqan/actions/.github/workflows/lint_autofix.yml. + +name: Lint Autofix + +on: + workflow_run: # zizmor: ignore[dangerous-triggers] -- privileged half; the called workflow treats PR data as data only + workflows: [CI] + types: [completed] + +permissions: {} + +jobs: + autofix: + name: Lint Autofix + uses: seqan/actions/.github/workflows/lint_autofix.yml@main + permissions: + contents: read + pull-requests: read + # Only these secrets. This job has no environment, but GitHub resolves them with the environment of the called job + # that uses them (`pr-bot`). + secrets: + SEQAN_PR_BOT_KEY: ${{ secrets.SEQAN_PR_BOT_KEY }} + SEQAN_ACTIONS_GPG_KEY: ${{ secrets.SEQAN_ACTIONS_GPG_KEY }} + SEQAN_ACTIONS_GPG_PASSPHRASE: ${{ secrets.SEQAN_ACTIONS_GPG_PASSPHRASE }}