name: akbasic Release Build run-name: ${{ gitea.actor }} akbasic release checks # Manual only. Nothing here runs on push: the full mutation set is thousands of # mutants and hours of runner time, which is a release-gate cost, not a # per-commit one. Trigger it from the Actions tab. on: workflow_dispatch: inputs: mutation_threshold: description: "Fail if the mutation score falls below this percentage" required: false default: "65" mutation_targets: description: "Space-separated files to mutate; empty means the whole src/ tree" required: false default: "" jobs: # Moved here from ci.yaml. The docs are wanted for a release, not on every # push, and building them beside the release mutation run keeps the two # artefacts a release needs in one place. docs: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v4 # No submodules at all. Doxyfile's INPUT is include/akbasic, src and # examples, none of which live under deps/, and doxygen does not need to # resolve to parse a declaration. Verified by running it # against a tree with no deps/ present. - name: dependencies run: | sudo apt-get update -y sudo apt-get install -y doxygen graphviz # This is a gate, not a convenience. The Doxyfile sets # WARN_AS_ERROR = FAIL_ON_WARNINGS, so a doc block that documents some of a # function's parameters but not all of them fails the build. All 114 public # declarations under include/akbasic carry a @brief, a @param per # parameter, a @return and their @throws; keep it that way. # # OUTPUT_DIRECTORY is build/docs and doxygen will not create a missing # parent, so make it first -- there is no cmake step in this job to do it. - name: build API documentation run: | mkdir -p build doxygen Doxyfile - name: upload API documentation if: always() uses: actions/upload-artifact@v4 with: name: api-documentation path: build/docs/html/ if-no-files-found: error - run: echo "🍏 This job's status is ${{ job.status }}." full_mutation: runs-on: ubuntu-latest # 3675 mutants across the whole src/ tree. Cost per mutant is one incremental # rebuild plus one full ctest run, which measures at roughly 2s for a leaf # file and 11s for src/value.c, where almost everything links against it. # Budget most of a day and expect it to finish well inside that; the ceiling # exists so a mutant that wedges the runner cannot hold it forever. timeout-minutes: 720 steps: - name: Check out repository code uses: actions/checkout@v4 with: # The harness copies the repo and configures a build inside the copy, # so it needs the same submodules the main build does -- including # deps/basicinterpret, because the golden corpus is part of what kills # mutants. submodules: true - name: dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake gcc python3 # The whole akbasic-owned src/ tree. ci.yaml runs a two-file subset on every # push; this is the one that actually covers the interpreter. # # Mutation testing is the only gate that sees the error-handling control # flow at all: libakerror's ATTEMPT/CATCH/PASS macros expand at their call # sites, so gcov attributes them to the caller and line coverage cannot # measure them. # # The default threshold matches ci.yaml's rather than being stricter. # Whole-tree coverage is uneven -- src/symtab.c is the only file measured # on the push path, at 74.1%, and the rest is unmeasured, so a tighter # number here would be a guess. Raise it with the workflow input once a # full run has established a real baseline. # # The two inputs arrive through the environment rather than being # interpolated straight into the script. ${{ }} substitution happens before # the shell sees the line, so a value containing shell metacharacters would # otherwise run as code. This workflow is manual and owner-triggered, but # the safe form costs nothing. - name: mutation testing (full tree) env: MUTATION_TARGETS: ${{ gitea.event.inputs.mutation_targets }} MUTATION_THRESHOLD: ${{ gitea.event.inputs.mutation_threshold }} run: | set -eu # Word-splitting is the point here: the input is a space-separated # list. An empty input leaves $targets empty and the harness falls # through to its own default, which is the whole src/ tree. targets="" for f in ${MUTATION_TARGETS:-}; do targets="$targets --target $f" done # shellcheck disable=SC2086 python3 scripts/mutation_test.py \ $targets \ --junit mutation-junit.xml \ --threshold "${MUTATION_THRESHOLD:-65}" # Publish even when the threshold gate fails, so survivors are visible -- # each one is a missing test. Display-only (fail_on_failure: false); the # --threshold above is the gate. annotate_only avoids the Checks API 404 # on Gitea (mikepenz/action-junit-report#23). - name: publish mutation results if: always() uses: mikepenz/action-junit-report@v4 with: report_paths: 'mutation-junit.xml' annotate_only: true detailed_summary: true include_passed: true fail_on_failure: 'false' # Keep the raw report as well as the annotations: a release wants the # survivor list on file, and the job summary is not durable. - name: upload mutation report if: always() uses: actions/upload-artifact@v4 with: name: mutation-report path: mutation-junit.xml if-no-files-found: warn - run: echo "🍏 This job's status is ${{ job.status }}."