diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..baf5f4c625 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,7 @@ +self-hosted-runner: + # Runner images newer than actionlint's list of GitHub-hosted labels. + labels: + - ubuntu-26.04 + - ubuntu-26.04-arm + - windows-11-vs2026-arm + - xcode-27 diff --git a/.github/actions/setup-build/action.yaml b/.github/actions/setup-build/action.yaml new file mode 100644 index 0000000000..f277b4d21f --- /dev/null +++ b/.github/actions/setup-build/action.yaml @@ -0,0 +1,114 @@ +name: Set up the build +description: >- + Installs the host packages, Python tools and vcpkg that configuring the + viewer needs, and adds the R2 vcpkg binary cache when asked to. Expects the + repository checked out with its submodules and VCPKG_ROOT set. + +inputs: + r2-cache-mode: + description: "read or readwrite to use the R2 vcpkg binary cache; empty to go without it" + required: false + default: "" + r2-endpoint-url: + description: "The R2 account's S3 endpoint" + required: false + default: "" + r2-bucket: + description: "The R2 bucket holding the cache" + required: false + default: "" + r2-access-key-id: + description: "R2 access key id" + required: false + default: "" + r2-secret-access-key: + description: "R2 secret access key" + required: false + default: "" + +runs: + using: composite + steps: + - name: Linux dependency install and disk cleanup + if: runner.os == 'Linux' + shell: bash + run: | + sudo apt update + sudo apt install -y \ + autoconf autoconf-archive automake bison build-essential cmake curl flex gettext \ + libasound2-dev libaudio-dev libdbus-1-dev libdecor-0-dev libdrm-dev \ + libegl1-mesa-dev libfribidi-dev libgbm-dev libgl1-mesa-dev libgl1-mesa-dri libgles2-mesa-dev \ + libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev libibus-1.0-dev libjack-dev libltdl-dev \ + libpipewire-0.3-dev libpulse-dev libsndio-dev libtext-unidecode-perl \ + libthai-dev libtool libudev-dev libunwind-dev liburing-dev libvlc-dev libwayland-dev \ + libx11-dev libxcursor-dev libxext-dev libxfixes-dev libxft-dev libxi-dev libxinerama-dev \ + libxkbcommon-dev libxrandr-dev libxss-dev libxtst-dev linux-libc-dev mold \ + nasm ninja-build pkgconf tar tex-common texinfo unzip zip + + sudo locale-gen en_US.UTF-8 + sudo locale-gen en_GB.UTF-8 + sudo locale-gen fr_FR.UTF-8 + + df -h + sudo docker container prune -f + sudo docker image prune -a -f + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/.ghcup + df -h + + - name: macOS Homebrew dependency install + if: runner.os == 'macOS' + shell: bash + run: brew install autoconf autoconf-archive automake libtool nasm unzip zip + + - name: Setup python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + + # Linux builds with the CMake floor, so the floor is a floor. + - name: Install python dependencies + shell: bash + run: | + if [[ "$RUNNER_OS" == "Linux" ]] + then pip3 install llsd cmake==4.0.0 ninja + else pip3 install llsd cmake ninja + fi + + - name: Bootstrap vcpkg Windows + if: runner.os == 'Windows' + shell: pwsh + run: ./vcpkg/bootstrap-vcpkg.bat + + - name: Bootstrap vcpkg non-Windows + if: runner.os != 'Windows' + shell: bash + run: ./vcpkg/bootstrap-vcpkg.sh + + # Ports built with cargo (Velopack) fetch their crates here. Keyed on the + # manifest and the registry baseline, which decide the crate versions. + - name: Cache Rust dependencies + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: ${{ runner.os }}-cargo-${{ hashFiles('indra/vcpkg.json', 'indra/vcpkg-configuration.json') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Install AWS CLI on macOS + if: inputs.r2-cache-mode != '' && runner.os == 'macOS' + shell: bash + run: brew install awscli + + - name: Configure R2 binary cache + if: inputs.r2-cache-mode != '' + shell: bash + env: + R2_CACHE_MODE: ${{ inputs.r2-cache-mode }} + R2_ENDPOINT_URL: ${{ inputs.r2-endpoint-url }} + R2_BUCKET: ${{ inputs.r2-bucket }} + R2_ACCESS_KEY_ID: ${{ inputs.r2-access-key-id }} + R2_SECRET_ACCESS_KEY: ${{ inputs.r2-secret-access-key }} + run: python scripts/vcpkg/configure_r2_cache.py diff --git a/.github/codeql/codeql-config.yaml b/.github/codeql/codeql-config.yaml new file mode 100644 index 0000000000..4bbe718b82 --- /dev/null +++ b/.github/codeql/codeql-config.yaml @@ -0,0 +1,18 @@ +name: Alchemy CodeQL config + +# security-extended adds the lower-precision security queries to the default +# set, the taint tracking among them: data from the network followed into +# parsers, allocations and file paths. A rule that proves too noisy for this +# codebase goes in query-filters, e.g. +# +# query-filters: +# - exclude: +# id: cpp/some-rule +queries: + - uses: security-extended + +# Only the languages read without a build (Python, Actions) honour paths: +# C/C++ analyses exactly what the build compiles. +paths-ignore: + - indra/externals + - vcpkg diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index a522dcc477..4452870f69 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,12 +1,14 @@ version: 2 updates: + # The workflows, and the composite actions they share. - package-ecosystem: github-actions - directory: / + directories: + - / + - /.github/actions/* schedule: interval: weekly - - package-ecosystem: vcpkg - directory: /indra # The location of vcpkg.json - schedule: - interval: weekly + # No vcpkg entry: Dependabot moves builtin-baseline but not the vcpkg + # submodule, and a baseline the submodule does not contain fails every + # configure. The two move together, by hand. diff --git a/.github/labeler.yaml b/.github/labeler.yaml index 96b8043824..8c832d620b 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -1,3 +1,7 @@ +alscript: +- changed-files: + - any-glob-to-any-file: indra/alscript/** + llappearance: - changed-files: - any-glob-to-any-file: indra/llappearance/** @@ -18,10 +22,6 @@ llcorehttp: - changed-files: - any-glob-to-any-file: indra/llcorehttp/** -llcrashlogger: -- changed-files: - - any-glob-to-any-file: indra/llcrashlogger/** - llfilesystem: - changed-files: - any-glob-to-any-file: indra/llfilesystem/** @@ -46,14 +46,14 @@ llmath: - changed-files: - any-glob-to-any-file: indra/llmath/** -llmeshoptimizer: -- changed-files: - - any-glob-to-any-file: indra/llmeshoptimizer/** - llmessage: - changed-files: - any-glob-to-any-file: indra/llmessage/** +llphysicsextensionsos: +- changed-files: + - any-glob-to-any-file: indra/llphysicsextensionsos/** + llplugin: - changed-files: - any-glob-to-any-file: indra/llplugin/** @@ -70,6 +70,10 @@ llui: - changed-files: - any-glob-to-any-file: indra/llui/** +llwebrtc: +- changed-files: + - any-glob-to-any-file: indra/llwebrtc/** + llwindow: - changed-files: - any-glob-to-any-file: indra/llwindow/** @@ -78,6 +82,10 @@ llxml: - changed-files: - any-glob-to-any-file: indra/llxml/** +media_plugins: +- changed-files: + - any-glob-to-any-file: indra/media_plugins/** + viewer: - changed-files: - any-glob-to-any-file: indra/newview/** @@ -115,3 +123,7 @@ objc: - any-glob-to-any-file: - '**/*.m' - '**/*.mm' + +github_actions: +- changed-files: + - any-glob-to-any-file: .github/** diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 33eb212bbc..3b47d277d7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,18 +22,40 @@ on: type: string default: "hippo" pull_request: + # Everything but documentation and the GitHub files no build reads; + # later patterns win. + paths: + - "**" + - "!**/*.md" + - "!doc/**" + - "!LICENSE" + - "!.github/**" + - ".github/workflows/build.yaml" + - ".github/actions/**" push: branches: ["main", "release/*", "beta/*", "project/*"] tags: ["Alchemy*"] +# Every job reads the repository and nothing more, unless it says otherwise. permissions: contents: read +# A new push to a pull request supersedes the run in flight. Everything else +# (branches, tags, dispatches) runs to completion. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: # The whole point of the setup job is that we want to set variables once # that will be consumed by multiple subsequent jobs. setup: runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + # which-branch looks up a tag's branch and that branch's pull request. + pull-requests: read outputs: viewer_branch: ${{ steps.which-branch.outputs.branch }} relnotes: ${{ steps.which-branch.outputs.relnotes }} @@ -43,20 +65,17 @@ jobs: sentry_dsn: ${{ steps.setvar.outputs.sentry_dsn }} viewer_channel: ${{ steps.setvar.outputs.viewer_channel }} is_private_build: ${{ steps.setvar.outputs.is_private_build }} + r2_cache_mode: ${{ steps.setvar.outputs.r2_cache_mode }} env: # Build with a tag like "Alchemy#abcdef0" to generate a release page # (used for builds we are planning to deploy). # When you want to use a string variable as a workflow YAML boolean, it's # important to ensure it's the empty string when false. If you omit || '', # its value when false is "false", which is interpreted as true. - RELEASE_RUN: ${{ (github.event.inputs.release_run || github.ref_type == 'tag' && startsWith(github.ref_name, 'Alchemy')) && 'Y' || '' }} + RELEASE_RUN: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'Alchemy') && 'Y' || '' }} FROM_FORK: ${{ github.event.pull_request.head.repo.fork }} steps: - - name: Checkout code - uses: actions/checkout@v7 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - + # which-branch installs PyGithub for tag builds. - name: Setup python uses: actions/setup-python@v7 with: @@ -77,6 +96,14 @@ jobs: # The Sentry project crash reports go to; unset, the viewer is # built without a crash reporter. SENTRY_DSN: ${{ vars.SENTRY_DSN }} + # Inputs and ref names reach the script through the environment, + # never pasted into it, so no input can become a command. + INPUT_CHANNEL: ${{ inputs.channel }} + INPUT_PROJECT: ${{ inputs.project }} + IS_EDU_TAG: ${{ github.ref_type == 'tag' && contains(github.ref_name, 'edu') }} + R2_ENABLED: ${{ vars.VCPKG_R2_ENABLED }} + REF_PROTECTED: ${{ github.ref_protected }} + PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} run: | echo "release_run=$RELEASE_RUN" >> "$GITHUB_OUTPUT" @@ -108,25 +135,42 @@ jobs: echo "sentry_dsn=" >> "$GITHUB_OUTPUT" fi + # The vcpkg binary cache in R2. Only trusted builds of protected + # refs write it; same-repository pull requests read it with the + # read-only keys, so code still under review cannot put packages in + # front of a release build. Forks get no secrets and Dependabot gets + # its own, so neither uses it. + r2_cache_mode= + if [[ "$R2_ENABLED" == "true" && "$GITHUB_ACTOR" != "dependabot[bot]" ]]; then + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + if [[ "$PR_HEAD_REPO" == "$GITHUB_REPOSITORY" ]]; then + r2_cache_mode="read" + fi + elif [[ "$REF_PROTECTED" == "true" ]]; then + r2_cache_mode="readwrite" + fi + fi + echo "r2_cache_mode=$r2_cache_mode" >> "$GITHUB_OUTPUT" + # determine the viewer channel from the branch or tag name # trigger an EDU build by including "edu" in the tag - edu=${{ github.ref_type == 'tag' && contains(github.ref_name, 'edu') }} - echo "ref_type=${{ github.ref_type }}, ref_name=${{ github.ref_name }}, edu='$edu'" + edu=$IS_EDU_TAG + echo "ref_type=$GITHUB_REF_TYPE, ref_name=$GITHUB_REF_NAME, edu='$edu'" branch=$BUILD_VCS_BRANCH - if [[ "${{inputs.channel}}" != "" && "${{inputs.channel}}" != "Auto" ]]; + if [[ "$INPUT_CHANNEL" != "" && "$INPUT_CHANNEL" != "Auto" ]]; then - if [[ "${{inputs.channel}}" == "Project" ]]; + if [[ "$INPUT_CHANNEL" == "Project" ]]; then - export viewer_channel="${{ env.VIEWER_CHANNEL_BASE }} Project ${{inputs.project}}" + export viewer_channel="$VIEWER_CHANNEL_BASE Project $INPUT_PROJECT" else - export viewer_channel="${{ env.VIEWER_CHANNEL_BASE }} ${{inputs.channel}}" + export viewer_channel="$VIEWER_CHANNEL_BASE $INPUT_CHANNEL" fi elif [[ "$edu" == "true" ]]; then - export viewer_channel="${{ env.VIEWER_CHANNEL_BASE }} Release edu" + export viewer_channel="$VIEWER_CHANNEL_BASE Release edu" elif [[ "$branch" == "develop" ]]; then - export viewer_channel="${{ env.VIEWER_CHANNEL_BASE }} Develop" + export viewer_channel="$VIEWER_CHANNEL_BASE Develop" else IFS='/' read -ra ba <<< "$branch" prefix=${ba[0]} @@ -135,21 +179,21 @@ jobs: prj_str="${prj[*]}" # uppercase first letter of each word capitalized=$(echo "$prj_str" | awk '{for (i=1; i<=NF; i++) $i = toupper(substr($i,1,1)) substr($i,2); print}') - export viewer_channel="${{ env.VIEWER_CHANNEL_BASE }} Project $capitalized" + export viewer_channel="$VIEWER_CHANNEL_BASE Project $capitalized" elif [[ "$prefix" == "release" || "$prefix" == "main" ]]; then - export viewer_channel="${{ env.VIEWER_CHANNEL_BASE }} Release" + export viewer_channel="$VIEWER_CHANNEL_BASE Release" else - export viewer_channel="${{ env.VIEWER_CHANNEL_BASE }} Test" + export viewer_channel="$VIEWER_CHANNEL_BASE Test" fi fi echo "viewer_channel=$viewer_channel" >> "$GITHUB_OUTPUT" build: needs: setup - permissions: - packages: write strategy: + # One platform failing says nothing about the others: let them finish. + fail-fast: false matrix: runner: ["windows-2025-vs2026", "xcode-27", "ubuntu-26.04"] configuration: [release] @@ -162,16 +206,8 @@ jobs: arch: arm64 - runner: windows-2025-vs2026 arch: arm64 - - runner: xcode-27 - configuration: relwithdebinfo - - runner: xcode-27 - configuration: optdebug - runner: xcode-27 arch: x64 - - build_variant: Viewer - configuration: relwithdebinfo - - build_variant: Viewer - configuration: optdebug include: # Linux on arm64: NEON is a first-class target, and this is the # one row where GCC compiles the SIMD layer for it. @@ -194,8 +230,7 @@ jobs: build_variant: Tests runs-on: ${{ matrix.runner }} - # Windows arm64 is allowed to fail, without cancelling the other rows, - # until the vlc-bin port has an arm64 Windows build. + # A failing test row is reported without failing the run. continue-on-error: ${{ startsWith(matrix.build_variant, 'Tests') }} outputs: viewer_version: ${{ steps.build.outputs.viewer_version }} @@ -234,116 +269,35 @@ jobs: master_message_template_checkout: ${{ github.workspace }}/.master-message-template # vcpkg Setup VCPKG_ROOT: ${{ github.workspace }}/vcpkg - VCPKG_USERNAME: ${{ github.repository_owner }} - # VCPKG_FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json VCPKG_BINARY_SOURCES: "clear;default,readwrite" steps: - - name: Linux Dependency Install and Disk Cleanup - if: runner.os == 'Linux' - run: | - sudo apt update - sudo apt install -y \ - autoconf autoconf-archive automake bison build-essential cmake curl flex gettext \ - libasound2-dev libaudio-dev libdbus-1-dev libdbus-1-dev libdecor-0-dev libdrm-dev \ - libegl1-mesa-dev libfribidi-dev libgbm-dev libgl1-mesa-dev libgl1-mesa-dri libgles2-mesa-dev \ - libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev libibus-1.0-dev libjack-dev libltdl-dev \ - libpipewire-0.3-dev libpulse-dev libsndio-dev libtext-unidecode-perl \ - libthai-dev libtool libudev-dev libunwind-dev liburing-dev libvlc-dev libwayland-dev \ - libx11-dev libxcursor-dev libxext-dev libxfixes-dev libxft-dev libxi-dev libxinerama-dev \ - libxkbcommon-dev libxrandr-dev libxss-dev libxtst-dev linux-libc-dev mold \ - nasm ninja-build pkgconf tar tex-common texinfo unzip zip - - sudo locale-gen en_US.UTF-8 - sudo locale-gen en_GB.UTF-8 - sudo locale-gen fr_FR.UTF-8 - - df -h - sudo docker container prune -f - sudo docker image prune -a -f - sudo rm -rf /usr/local/lib/android - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/.ghcup - df -h - - - name: macOS Homebrew Dependency Install - if: runner.os == 'macOS' - run: | - brew install autoconf autoconf-archive automake libtool nasm unzip zip - - name: Checkout code uses: actions/checkout@v7 with: + # Full history: the vcpkg submodule needs it to resolve the + # versions the manifest pins. fetch-depth: 0 + persist-credentials: false submodules: recursive ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Checkout master-message-template uses: actions/checkout@v7 with: + persist-credentials: false repository: secondlife/master-message-template path: .master-message-template - - name: Setup python - uses: actions/setup-python@v7 + - name: Set up the build + uses: ./.github/actions/setup-build with: - python-version: "3.14" - - # Linux builds with the CMake floor, so the floor is a floor. - - name: Install python dependencies - shell: bash - run: | - if [[ "$RUNNER_OS" == "Linux" ]] - then pip3 install llsd cmake==4.0.0 ninja - else pip3 install llsd cmake ninja - fi - - - name: Bootstrap vcpkg Windows - if: runner.os == 'Windows' - run: | - ./vcpkg/bootstrap-vcpkg.bat - - - name: Bootstrap vcpkg non-Windows - if: runner.os == 'macOS' || runner.os == 'Linux' - run: | - ./vcpkg/bootstrap-vcpkg.sh - - - name: Install AWS CLI on macOS - if: >- - vars.VCPKG_R2_ENABLED == 'true' && runner.os == 'macOS' && - ((github.ref_protected && github.event_name != 'pull_request') || - (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)) - run: brew install awscli - - - name: Configure R2 binary cache for trusted builds - if: vars.VCPKG_R2_ENABLED == 'true' && github.ref_protected && github.event_name != 'pull_request' - env: - R2_CACHE_MODE: readwrite - R2_ENDPOINT_URL: ${{ vars.VCPKG_R2_ENDPOINT_URL }} - R2_BUCKET: ${{ vars.VCPKG_R2_BUCKET }} - R2_ACCESS_KEY_ID: ${{ secrets.VCPKG_R2_ACCESS_KEY_ID }} - R2_SECRET_ACCESS_KEY: ${{ secrets.VCPKG_R2_SECRET_ACCESS_KEY }} - run: python scripts/vcpkg/configure_r2_cache.py - - - name: Configure R2 binary cache for same-repository PRs - if: >- - vars.VCPKG_R2_ENABLED == 'true' && github.event_name == 'pull_request' && - github.event.pull_request.head.repo.full_name == github.repository - env: - # Temporary cache population test; restore read mode and reader secrets afterward. - R2_CACHE_MODE: readwrite - R2_ENDPOINT_URL: ${{ vars.VCPKG_R2_ENDPOINT_URL }} - R2_BUCKET: ${{ vars.VCPKG_R2_BUCKET }} - R2_ACCESS_KEY_ID: ${{ secrets.VCPKG_R2_ACCESS_KEY_ID }} - R2_SECRET_ACCESS_KEY: ${{ secrets.VCPKG_R2_SECRET_ACCESS_KEY }} - run: python scripts/vcpkg/configure_r2_cache.py - - - name: Cache Rust Dependencies - uses: actions/cache@v6 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + r2-cache-mode: ${{ needs.setup.outputs.r2_cache_mode }} + r2-endpoint-url: ${{ vars.VCPKG_R2_ENDPOINT_URL }} + r2-bucket: ${{ vars.VCPKG_R2_BUCKET }} + # The writer keys only where the cache is written; a reader's mode + # alone would not stop code that finds writer keys from using them. + r2-access-key-id: ${{ needs.setup.outputs.r2_cache_mode == 'readwrite' && secrets.VCPKG_R2_ACCESS_KEY_ID || secrets.VCPKG_R2_READ_ACCESS_KEY_ID }} + r2-secret-access-key: ${{ needs.setup.outputs.r2_cache_mode == 'readwrite' && secrets.VCPKG_R2_SECRET_ACCESS_KEY || secrets.VCPKG_R2_READ_SECRET_ACCESS_KEY }} - name: Configure id: configure @@ -407,7 +361,7 @@ jobs: -DAL_USE_SENTRY:BOOL="$AL_USE_SENTRY" \ -DAL_SENTRY_DSN:STRING="$AL_SENTRY_DSN" \ -DAL_CHANNEL:STRING="${AL_CHANNEL}" \ - -DAL_GRID:STRING="\"$VIEWER_GRID\"" \ + -DAL_GRID:STRING="$VIEWER_GRID" \ $template_verifier_master_url \ "${SIGNING[@]}" @@ -430,15 +384,15 @@ jobs: if $BUILD_TESTS then # Now build the tests. We want to build them in a separate step before running them. - cmake --build "$BUILD_DIRECTORY" --config "$cmake_config" --parallel $(nproc) + cmake --build "$BUILD_DIRECTORY" --config "$cmake_config" --parallel "$(nproc)" # Now that we've built the tests, we can run them to validate the build. # If the tests fail, their output will be visible in the logs due to # CTEST_OUTPUT_ON_FAILURE above, and the build step will be marked as failed. - cmake --build "$BUILD_DIRECTORY" --config "$cmake_config" --target BUILD_AND_RUN_TESTS --parallel $(nproc) + cmake --build "$BUILD_DIRECTORY" --config "$cmake_config" --target BUILD_AND_RUN_TESTS --parallel "$(nproc)" elif $AL_BUILD_VIEWER then - cmake --build "$BUILD_DIRECTORY" --config "$cmake_config" --parallel $(nproc) + cmake --build "$BUILD_DIRECTORY" --config "$cmake_config" --parallel "$(nproc)" else echo "::notice::Skipping build due to both viewer and tests being disabled" fi @@ -465,17 +419,10 @@ jobs: fi # The package: the install tree on Windows, which the signing job - # packages with Velopack; the CPack archive elsewhere. The names - # the packaging steps need come from the configure. + # packages with Velopack from it and its package.env metadata; the + # CPack archive elsewhere. if $AL_BUILD_VIEWER then - # The job's outputs come from whichever row sets them last. - # Keep the shared Velopack outputs on x64; Windows packaging - # reads per-architecture metadata artifacts below. - if [[ "$RUNNER_OS" != "Windows" || "$BUILD_ARCH" == "x64" ]] - then - cat "$BUILD_DIRECTORY/newview/package.env" >> "$GITHUB_OUTPUT" - fi case "$RUNNER_OS" in Windows) cmake --install "$BUILD_DIRECTORY" --config "$cmake_config" --prefix "$RUNNER_TEMP/app" @@ -507,7 +454,9 @@ jobs: with: name: "${{ steps.build.outputs.artifact }}-app" if-no-files-found: error - compression-level: 9 + # The Windows install tree is uncompressed; the .tar.xz and .dmg + # already are. + compression-level: ${{ runner.os == 'Windows' && 9 || 0 }} path: | ${{ steps.build.outputs.viewer_app }} @@ -604,17 +553,21 @@ jobs: sign-and-package-windows: needs: [setup, build] + timeout-minutes: 30 strategy: matrix: arch: [x64, arm64] runs-on: windows-2025-vs2026 steps: + # The app and its metadata come from the build's artifacts; the tree + # contributes only the Velopack tool manifest. - name: Checkout code uses: actions/checkout@v7 with: persist-credentials: false - submodules: recursive ref: ${{ github.event.pull_request.head.sha || github.sha }} + sparse-checkout: /dotnet-tools.json + sparse-checkout-cone-mode: false - name: Setup .NET for Velopack uses: actions/setup-dotnet@v6 @@ -764,8 +717,10 @@ jobs: release: needs: [setup, build, sign-and-package-windows] runs-on: ubuntu-latest + timeout-minutes: 30 if: needs.setup.outputs.release_run permissions: + # Creates the release, generates its notes and uploads its assets. contents: write steps: - uses: actions/download-artifact@v8 @@ -791,7 +746,9 @@ jobs: with: # name the release page for the branch. We want channel and version. name: "${{ needs.setup.outputs.viewer_channel }} ${{ needs.build.outputs.viewer_version }}" - body: | + # The tag's pull request notes after "relnotes:", if any, ahead of + # the generated notes. + body: ${{ needs.setup.outputs.relnotes }} prerelease: true generate_release_notes: true target_commitish: ${{ github.sha }} @@ -803,5 +760,7 @@ jobs: Windows*-releases/* - name: post release URL + env: + RELEASE_URL: ${{ steps.release.outputs.url }} run: | - echo "::notice::Release ${{ steps.release.outputs.url }}" + echo "::notice::Release $RELEASE_URL" diff --git a/.github/workflows/check-pr.yaml b/.github/workflows/check-pr.yaml index 68ca8244c0..bf0997b11b 100644 --- a/.github/workflows/check-pr.yaml +++ b/.github/workflows/check-pr.yaml @@ -4,12 +4,13 @@ on: pull_request: types: [opened, edited, reopened, synchronize] -permissions: - contents: read +# The check reads the event payload; it makes no API calls. +permissions: {} jobs: check-description: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Check PR description uses: actions/github-script@v9 diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 0000000000..71894f9940 --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,142 @@ +name: CodeQL + +on: + push: + branches: ["develop", "main"] + pull_request: + branches: ["develop", "main"] + # Nothing CodeQL reads: skins, settings, shaders, docs. + paths-ignore: + - "**/*.md" + - "doc/**" + - "indra/newview/app_settings/**" + - "indra/newview/character/**" + - "indra/newview/skins/**" + schedule: + # Weekly, so new queries reach code nobody has touched since. + - cron: "17 3 * * 1" + workflow_dispatch: + +permissions: + contents: read + +# A pull request analyses only its latest push. A branch keeps one analysis +# running and the newest waiting, so a busy develop still finishes some. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + analyze: + name: Analyze (${{ matrix.name }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: ${{ matrix.build-mode == 'manual' && 360 || 30 }} + permissions: + # Reads the run the results belong to. + actions: read + contents: read + # Uploads the results to code scanning. + security-events: write + strategy: + fail-fast: false + matrix: + include: + - name: actions + language: actions + build-mode: none + runner: ubuntu-latest + category: /language:actions + - name: python + language: python + build-mode: none + runner: ubuntu-latest + category: /language:python + # C/C++ is traced through a real build, so the analysis sees the + # code as compiled, every vcpkg header resolved and every platform + # #if decided. One row per platform layer. Linux keeps the category + # default setup used, so its alerts and their dismissals carry over. + # macOS adds little: CodeQL does not read Objective-C++. + - name: c-cpp, Linux + language: c-cpp + build-mode: manual + runner: ubuntu-26.04 + preset: ninja-os-mold + triplet: x64-linux-alchemy-avx2-release + category: /language:c-cpp + - name: c-cpp, Windows + language: c-cpp + build-mode: manual + runner: windows-2025-vs2026 + preset: vs2026-os-x64 + triplet: x64-windows-alchemy-avx2-release + category: /language:c-cpp/os:windows + env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + VCPKG_BINARY_SOURCES: "clear;default,readwrite" + # MSBuild keeps its worker nodes alive between builds. One started by + # the configure, before tracing began, would compile untraced. + MSBUILDDISABLENODEREUSE: 1 + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + # The build needs the submodules, and vcpkg its full history to + # resolve the versions the manifest pins. + fetch-depth: ${{ matrix.build-mode == 'manual' && '0' || '1' }} + persist-credentials: false + submodules: ${{ matrix.build-mode == 'manual' && 'recursive' || 'false' }} + + - name: Set up the build + if: matrix.build-mode == 'manual' + uses: ./.github/actions/setup-build + with: + # Read only, with the read-only keys: an analysis has nothing to add + # to the cache. + r2-cache-mode: >- + ${{ vars.VCPKG_R2_ENABLED == 'true' && github.actor != 'dependabot[bot]' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && + 'read' || '' }} + r2-endpoint-url: ${{ vars.VCPKG_R2_ENDPOINT_URL }} + r2-bucket: ${{ vars.VCPKG_R2_BUCKET }} + r2-access-key-id: ${{ secrets.VCPKG_R2_READ_ACCESS_KEY_ID }} + r2-secret-access-key: ${{ secrets.VCPKG_R2_READ_SECRET_ACCESS_KEY }} + + # The configure installs the vcpkg ports, so it runs before tracing + # starts: the database holds the viewer, not its dependencies. The + # build tree sits outside the checkout, which keeps the vcpkg headers + # out of the source root and so out of the alerts. Precompiled headers + # stay on, as in the build: the extractor reads the forced-include + # header as text, and the tree does not build without them. + - name: Configure + if: matrix.build-mode == 'manual' + shell: bash + env: + PRESET: ${{ matrix.preset }} + TRIPLET: ${{ matrix.triplet }} + run: | + set -x + cmake -S indra --preset "$PRESET" -B "$RUNNER_TEMP/codeql-build" \ + -DVCPKG_TARGET_TRIPLET="$TRIPLET" \ + -DAL_BUILD_TESTS:BOOL=OFF \ + -DAL_BUILD_PACKAGE:BOOL=OFF \ + -DAL_USE_VELOPACK:BOOL=ON \ + -DAL_USE_LTO:BOOL=OFF \ + -DAL_USE_TRACY:BOOL=OFF + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + config-file: ./.github/codeql/codeql-config.yaml + + # The runner's own shell on each platform: pwsh on Windows, where the + # tracer follows MSBuild's processes. + - name: Build + if: matrix.build-mode == 'manual' + run: cmake --build "${{ runner.temp }}/codeql-build" --config Release --parallel + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v4 + with: + category: ${{ matrix.category }} diff --git a/.github/workflows/label.yaml b/.github/workflows/label.yaml index bb3a9031e7..d08819da60 100644 --- a/.github/workflows/label.yaml +++ b/.github/workflows/label.yaml @@ -2,8 +2,13 @@ name: Pull Request Labeler on: - pull_request_target +permissions: {} + jobs: triage: + # Applying labels needs pull-requests: write. Creating one would need + # issues: write as well, so every label in labeler.yaml must already + # exist in the repository. permissions: contents: read pull-requests: write diff --git a/.github/workflows/lint-workflows.yaml b/.github/workflows/lint-workflows.yaml new file mode 100644 index 0000000000..ad99e227c1 --- /dev/null +++ b/.github/workflows/lint-workflows.yaml @@ -0,0 +1,38 @@ +name: Lint workflows + +on: + pull_request: + paths: [".github/**"] + push: + branches: ["develop"] + paths: [".github/**"] + +permissions: + contents: read + +jobs: + actionlint: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - name: Install actionlint + run: pip install actionlint-py==1.7.12.25 + + # Syntax, expressions, matrix and runner labels, the inputs of the + # actions called, and the run scripts through shellcheck. + - name: Run actionlint + env: + # shellcheck's errors and warnings fail the check; its info and + # style notes do not. + SHELLCHECK_OPTS: --severity=warning + run: actionlint -color diff --git a/.github/workflows/tag-release.yaml b/.github/workflows/tag-release.yaml index 41a215033a..1e36d3a9c6 100644 --- a/.github/workflows/tag-release.yaml +++ b/.github/workflows/tag-release.yaml @@ -26,19 +26,22 @@ on: description: "Override the tag name (optional). If the tag already exists, a numeric suffix is appended." required: false +# The tag is created with a personal access token, not the GITHUB_TOKEN. +permissions: {} + jobs: tag-release: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - name: Setup Env Vars - run: | - CHANNEL="${{ inputs.channel }}" - echo VIEWER_CHANNEL="Alchemy_${CHANNEL:-Develop}" >> ${GITHUB_ENV} - NIGHTLY_DATE=$(date --rfc-3339=date) - echo NIGHTLY_DATE=${NIGHTLY_DATE} >> ${GITHUB_ENV} - echo TAG_ID="$(echo ${{ github.sha }} | cut -c1-8)-${{ inputs.project || '${NIGHTLY_DATE}' }}" >> ${GITHUB_ENV} - name: Create Tag uses: actions/github-script@v9 + env: + # Inputs reach the script through the environment, never pasted + # into it, so no input can become code. + CHANNEL: ${{ inputs.channel }} + PROJECT: ${{ inputs.project }} + TAG_OVERRIDE: ${{ inputs.tag_override }} with: # use a real access token instead of GITHUB_TOKEN default. # required so that the results of this tag creation can trigger the build workflow @@ -47,8 +50,11 @@ jobs: # this token will need to be renewed anually in January github-token: ${{ secrets.LL_TAG_RELEASE_TOKEN }} script: | - const override = `${{ inputs.tag_override }}`.trim(); - const baseTag = override || `${{ env.VIEWER_CHANNEL }}#${{ env.TAG_ID }}`; + // A scheduled run has no inputs: a Develop build tagged with the date. + const channel = process.env.CHANNEL || 'Develop'; + const suffix = process.env.PROJECT || new Date().toISOString().slice(0, 10); + const override = (process.env.TAG_OVERRIDE || '').trim(); + const baseTag = override || `Alchemy_${channel}#${context.sha.slice(0, 8)}-${suffix}`; // Try the base tag first, then append -2, -3, etc. if it already exists let tag = baseTag;