CI, the toolchain image, and releases
How the GitLab pipeline is wired, why the prebuilt toolchain image exists, and how a release is cut. Pipeline/job orchestration is defined in .gitlab-ci.yml, while the shared core validation gate is the releaseCoreVerification task in the root build.gradle.kts. The scripts under stig-code-analyzer/scripts/ remain authoritative for the native toolchain. This page explains the shape and the why so a change doesn't surprise you.
All STIG GitLab CI Gradle invocations use --non-interactive (supported by the pinned Gradle 9.7.1) so Gradle uses defaults instead of waiting for interactive input. This flag does not control prompts from subprocesses. The optional ENHANCED_GRAPH_ORDERING preview remains disabled; upgrading Gradle does not require changing dependency or classpath ordering.
Developer CI commands
Run these three root tasks from the STIG source checkout. Their Kotlin implementation and exact CLI binary/checksum pins live in gradle/gitlab-ci.gradle.kts; they are not tasks in the published consumer plugin.
Task | Coordinator and source | Lifecycle |
|---|---|---|
|
| Checks authentication by default; login, commit CI status/logs, pipeline listing and triggering are explicit actions. |
| Official GitLab Runner polls GitLab for jobs. | Manages a systemd user service on the machine running Gradle. |
|
| Executes one command and exits; no Runner service is involved. |
The supported developer host is Linux on amd64 or arm64. GitLab control needs Git, curl, and tar. Local CI also needs rsync and rootless Podman; Runner service control uses rootless Podman and systemd user services. Fedora supplies the host tools through its normal packages. Full Fedora scripts/setup.sh also installs the distribution's glab and jq packages for direct GitLab CLI/API use. The Gradle tasks install their pinned glab, gitlab-runner, and gitlab-ci-local binaries on first use under ~/.cache/stig/gitlab-tools/, verifying downloaded distributions against checked-in SHA-256 checksums. They do not change the system's installed tools or npm dependencies. help --task <name> lists the current options; invocation controls use task options rather than -P.
Set up GitLab access and inspect pipelines
login runs glab auth login --hostname <origin-host> --web and verifies authentication with glab auth status. Complete the browser authorization when it opens. setup performs the same login and authentication check, then lists recent project pipelines with glab ci list --repo <origin-project>. The Git remote origin supplies the host and project; this checkout uses gitlab.com/safetygence-oss/stig/stig. status is the default action and can also be selected explicitly. It reports authentication status, including an actionable failure when login is missing. pipelines verifies authentication and lists pipelines. The tasks use glab's normal authentication store; GitLab account authentication is separate from a Runner token. An existing browser session or SSH Git access does not authenticate glab's API requests; complete login to authorize the CLI.
For other CLI or API operations, supply one raw command:
The initial glab is optional. Arguments are passed directly to the pinned executable from the checkout directory; glab resolves its own repository context, and native options such as --repo can select another project where supported. Raw mode runs only that command, without an additional authentication check or built-in action. Single and double quotes group arguments, empty quoted arguments are preserved, and backslashes escape characters outside single quotes. Inside double quotes, backslashes are preserved except before \, ", $, a backtick, or a newline. There is no shell expansion: variables, wildcards, pipes, redirects, and command substitutions remain literal arguments. Run ordinary glab in your terminal when using a shell pipeline with jq.
--raw-command is mutually exclusive with --action and the trigger/CI-inspection options. Standard input is forwarded for raw commands and browser login. Raw arguments are ordinary Gradle command-line inputs and may appear in logs and the configuration cache; keep credentials in glab's authentication store or use its file/stdin facilities. The former gitlabTrigger task is now gitlabCtrl --action trigger.
Inspect CI for a submitted commit
ciStatus resolves the local revision (default HEAD) when the task executes, finds the newest GitLab pipeline for that exact commit, and reports the pipeline and current job attempts. The host/project come from origin; the task does not fetch, push, trigger, retry, or modify CI. A local commit that has not been pushed can therefore have no pipeline. Configuration-cache reuse still resolves the current revision and makes fresh API requests. status without the ci prefix continues to mean authentication status.
The default is one observation without waiting for completion. --wait also waits for delayed pipeline creation, then keeps the selected pipeline ID even if another pipeline starts for the same commit. Polling defaults to every 30 seconds with a 7200-second deadline; use --poll-interval-seconds and --wait-timeout-seconds with --wait to change those limits. A pipeline blocked on a required manual action returns immediately instead of waiting for the deadline. Optional manual jobs do not prevent a successful pipeline from completing.
Each observation saves build/gitlab-ci/<commit-sha>/status.json. Use --output-directory <directory> to select a different parent directory. The JSON records the commit/project, pipeline URL and status, current jobs, observation outcome, completion flag, wait outcome, timestamp, and saved log paths or retrieval errors. outcome distinguishes observed, not_found, timed_out, manual_action_required, and error. Authentication/API failures are errors, not evidence that no pipeline exists. complete means a terminal pipeline state, not necessarily success: failed, canceled, and skipped pipelines are terminal too.
Both actions download failed-job traces automatically to <output-directory>/<commit-sha>/pipeline-<pipeline-id>/job-<job-id>.log, printing a bounded excerpt and the complete log path. ciLog also downloads logs for other jobs that have started; logs for running jobs are snapshots, not a live stream. Repeat the action to refresh them. A failed download is reported without replacing a previously saved log. Jobs marked allow_failure remain visible with their logs but do not turn a successful GitLab pipeline into a failed one.
A failed or canceled pipeline, failed API/log request, wait timeout, or blocking manual action during a wait gives the Gradle command a nonzero exit after saving its report. A nonwaiting observation of running, missing, or manual CI can exit zero, as can a skipped pipeline; this means the observation succeeded, not that CI passed. Automation must inspect the JSON's status, outcome, and complete rather than treating command exit zero as CI success.
Checked submissions use these actions for a separate advisory snapshot; see CI observation after checked submissions.
Execute the working tree locally
run is the default action. With no selector, the task runs automatic jobs chosen by the YAML for the current branch in a push-pipeline context. --pipeline makes that default explicit; it is mutually exclusive with --job. --job includes its needs dependencies, and --list previews job selection. Manual jobs remain manual during a whole-pipeline run. Explicit job names are checked against the local tool's evaluated rules, including when: never, before execution. Local jobs run sequentially by default; --concurrency <positive integer> changes that limit. Use registry-qualified image references: Podman cannot prompt to resolve short names in non-interactive jobs.
The task takes an isolated snapshot of tracked and unignored files, including unstaged edits, new files, executable permissions, symbolic links, and deletions. It stages only that disposable snapshot for gitlab-ci-local, whose native isolation otherwise excludes untracked files. The original index and checkout are unchanged. Actual branch, default branch, commit title, and commit message are supplied to the rule evaluator. If origin/HEAD is missing, establish it with git remote set-head origin --auto before running. Detached checkouts require checking out a branch.
Local execution sets STIG_CI_LOCAL=true and supplies the explicit job selector as STIG_CI_LOCAL_JOB. Selecting build_toolchain_image runs the same Kaniko job and Dockerfile locally, without registry credentials or image/cache publishing. Kaniko writes directly to a project-owned host mount, avoiding the extra multi-GB copies made by ordinary job artifact export. The result is .gitlab-ci-local/stig/artifacts/toolchain-image.tar and can be loaded with podman load --input <archive>. Each image run replaces this archive, retaining its current content-derived tag. This job is omitted from automatic local pipelines and from the dependencies of other explicitly selected local jobs.
Explicit rules in .gitlab-ci.yml omit release operations and Pages deployment locally. The launcher does not keep a second job allowlist. For other local jobs, it supplies the toolchain hash computed from the same ordered inputs as CI and reuses that published image. If a changed hash has not been built yet, pulling it fails; there is no fallback to latest or another toolchain. For a deliberate replay of an earlier pipeline, supply its exact 12-hex tag with --toolchain-hash <hash>. Remove that override when the new image is published.
build_and_test requires STIG_RESTRICTED_KEYWORDS. Put local job variables in the native gitlab-ci-local YAML format in the ignored .gitlab-ci-local-variables.yml, or select an external file with --variables-file <path>:
Supply the real project list; the launcher does not invent one or fetch GitLab secrets. Private image pulls use Podman's normal registry authentication. A local run can download images, dependencies, and included YAML; it does not create a GitLab pipeline.
State lives under .gitlab-ci-local/stig/. The worktree/.gitlab-ci-local/ directory contains the native tool's logs, artifacts, and cache. A separate project-owned Podman store holds its containers, volumes, and images; the task checks the selected store before operating on it. This store is separate from ordinary Podman and the official Runner, so images used by both modes occupy space in both stores. status reports local resource and disk use. clean refuses while a local run or its containers are active, then removes disposable outputs (including the directly exported image archive), stopped containers, and unused volumes/networks. Images are retained for reruns unless --images requests removal of unused images as well. The local tool normally removes job containers after execution. Interrupted runs can be cleaned with the same command.
A successful local build with more disk does not prove a hosted job fits its runner's temporary disk. Compare the job's df/du diagnostics, including Gradle caches and functional-test outputs, when investigating CI space exhaustion.
Register and control an official Runner
Create a project Runner in GitLab's Settings → CI/CD → Runners, assign a unique tag such as stig-workstation, and disable Run untagged jobs. Save its modern glrt- authentication token in a private file outside the checkout. Then, on the workstation or server that will execute jobs:
Run these as the rootless Podman user, without sudo. setup registers once and writes a systemd user unit; repeating setup uses the existing registration and does not need the token file. Configuration and build workspaces live under ~/.local/share/stig/gitlab-runner/<checkout-id>/. The task prints the exact service name and configuration path. start starts that service and its podman.socket dependency, then returns. stop requests graceful Runner shutdown, allowing current jobs to finish. status, also the default action, reports the service and verifies registration with GitLab. The executor uses Podman's local Unix socket; jobs do not receive the host socket as a volume.
setup --enable additionally enables the service for future user sessions. For startup before login and continued operation after logout, configure systemd user lingering through the host's normal loginctl enable-linger <user> administration. Runner needs outbound HTTPS to poll GitLab; no inbound port or firewall forwarding is required.
Trigger GitLab and choose a runner
The first invocation installs pinned glab if needed and directs you to gitlabCtrl --action setup when authentication is missing. --ref defaults to the current branch; the task neither commits nor pushes the working tree. The pipeline uses the ref currently stored in GitLab, including its CI configuration and release rules.
--runner-tag sets the pipeline's STIG_RUNNER_TAG. The routing configuration must first be present on the pushed ref. The YAML default is GitLab's saas-linux-small-amd64 hosted runner. build_and_test and release_and_publish set job-level STIG_RUNNER_TAG defaults to saas-linux-medium-amd64 for their full builds. Other jobs inherit small. Medium provides 4 vCPUs, 16 GB RAM, and 50 GB of temporary disk per job. That disk is the total allocation; system files, container images, caches, and build outputs consume it. Both sizes are available on GitLab Free, but medium consumes 2 compute minutes per job minute, compared with 1 for small. See GitLab's runner specifications and compute-minute accounting.
Pipeline or project CI/CD variables override both the global and job-level YAML defaults, so --runner-tag still routes all jobs to the selected runner. An own runner accepts jobs only when its registered tags match and it is available. Set STIG_RUNNER_TAG in project CI/CD variables to use an own runner for all pipelines. Leave the hosted defaults and pass --runner-tag only for explicitly requested own-runner pipelines to keep automatic pushes on hosted runners. gitlabCtrl never starts or stops a Runner service.
Repeat --variable KEY=VALUE for non-secret pipeline controls. For sensitive or structured values, use --variables-file <path>, which forwards a native glab JSON variables file through --variables-from without putting its values on the command line. Job selection on GitLab is governed by pipeline rules or native manual-job operations; the task does not implement an arbitrary standalone --job submission to Runner.
Pipeline at a glance
Seven stages run in order:
Stage | Job(s) | What happens |
|---|---|---|
release-prepare |
| Only when a release is requested: patch the changelog, commit release notes, and export the release commit SHA. |
toolchain |
| Build/refresh the Fedora primary and Ubuntu compatibility images (PhASAR + OS toolchain). |
build |
| Compile + test, run IntelliJ plugin verification, and render the two doc sites. |
release-tagging |
| Push the |
pages |
| Assemble the Writerside user guide + the Dokka API reference into one GitLab Pages site. |
release |
| After the validated release tag exists: publish to Maven Central, the Gradle Plugin Portal, and the JetBrains Marketplace. |
release-consumers |
| After artifact publication succeeds, dispatch independent update pipelines for the configured user projects. |
Resource diagnostics
Every job records resources at the start of before_script and after_script. The first snapshot follows cache restoration and artifact download; the final snapshot precedes report collection, cache saving and artifact upload. The snapshots report CPU architecture/count, OS memory, filesystem capacity and selected cache/build-directory sizes. Directory-size collection has a ten-second total limit. Missing utilities or counters do not fail the job.
Where cgroup v2 exposes them, the log also includes memory.current, memory.max, memory.peak, memory.events, CPU quota and the effective CPU set, with units identified. These describe the current process cgroup; tighter ancestor limits can also apply. The OS memory view from free can differ from the container's limits, and cgroup peak memory is not a Java heap measurement. after_script also runs after ordinary script failures, but GitLab does not guarantee it after every timeout, runner failure or container termination. Its snapshot does not measure the later report-copying and upload work.
The prebuilt toolchain image
stig-code-analyzer links against PhASAR, which can take ~10–30 min to compile from source. Recompiling them on every pipeline was the bottleneck, so a prebuilt image carries them — see ci/toolchain.Dockerfile.
What's baked in:
Fedora 44 and JDK 25 for the primary image; the release-only compatibility image retains Ubuntu 24.04 and JDK 25,
on Fedora, pinned GCC 15 compatibility headers/libraries for the unchanged LLVM 18 analysis compiler,
the OS toolchain via
scripts/setup.sh(LLVM 18 / clang / lld / z3 / libcurl / cmake / ninja) — see the three-jobs model,jq,ripgrep, andutil-linuxfor submission-script and GitLab CLI fixture tests on both distributions; image creation checksjq,rg, andflockbefore Gradle dependency prewarming,Xvfb, Xauthority, the JBR X11 and Skiko OpenGL runtime libraries, Fontconfig and Noto Sans/Serif/Mono for the rendered IDEA editor tests. Ubuntu installs these runtime libraries explicitly; Chromium's dependencies do not cover the IDEA runtime. Both build/test jobs, the release rebuild, and local container verification run Gradle under
xvfb-runwith a 1920×1080 virtual display. On Linux,prepareRenderedTestFontsgives those test JVMs a Fontconfig file that includes the system configuration and the selected IDEA runtime's bundled fonts. This keeps both Compose's system families and the SDK theme fonts available when the invoking Remote Development IDE exports its private Fontconfig environment,exact pinned Node.js from
scripts/toolchain-pins.env, plus the npm bundled with that Node release and the lockedjs-vendordependencies used by Dokka, browser tests, and Revideo,Playwright Chromium installed through the locked local
stig-gradle-plugin/js-vendor/node_modules/.bin/playwrightpackage, not throwawaynpx,Puppeteer's matching Chrome and Chrome Headless Shell installed by its approved npm installer for Revideo,
system FFmpeg/FFprobe for documentation-media probing, verification, and poster extraction; Revideo's final mux uses its package-locked FFmpeg,
a download-only seed Gradle cache for the pinned IDEA archive (including its bundled Kotlin/Java plugins and JBR) and verifier/signing tools, produced by
:stig-idea-plugin:prewarmIntellijPlatformCache. Prewarming resolves the archive configuration without extracting IDEA; jobs extract it and resolve their IntelliJ test framework in their writable cache. Plugin Verifier reuses that current pinned IDEA instead of resolving and extracting a second copy of the same distribution. Jobs consume the retainedmodules-2directly throughGRADLE_RO_DEP_CACHE; they do not copy the seed into their writable cache,the prebuilt PhASAR install pinned by
scripts/pins.env, produced bybuild-analyzer.sh --no-build-analyzer --no-local-phasar(PhASAR only — the analyzer's own C++ is compiled per-pipeline against this baked install). Onlyinstall/is kept; the build trees + source clones are dropped before IDEA prewarming, and the unused standalonephasar-cliis not built.
OS package installation finishes before PhASAR records its toolchain identity. Image creation and both native CI gates run build-analyzer.sh --verify-installation --no-build-analyzer --no-local-phasar to verify the installed file inventory, native recipe, compiler/header/library bytes, and packaged pin without cloning or compiling. A missing or mismatched baked installation fails before Gradle starts native work. After verification, Gradle's pin-specific producer can copy that tested installation without modifying the image's original install. Both jobs assert that neither the old nor the pin-specific PhASAR source clone appeared.
How it stays correct and fast: PINS_HASH
Prewarming configures the monorepo's Gradle build. The image therefore includes the applied developer-task scripts under gradle/, the included convention build's main sources, all dependency locks, and the toolchain input inventory under ci/. These are also allowed by .dockerignore and covered by the image hash. This does not build STIG's product sources or satisfy the later releaseCoreVerification gate.
build_toolchain_image tags the image by a hash of exactly the inputs that change what gets baked:
ci/toolchain-inputs.txt is the shared ordered inventory for both YAML image jobs, the ciToolchain* tasks, and localRunnerCtrl. Keep it in sync with the Dockerfile's configuration/setup inputs. .dockerignore restricts the build context to provisioning scripts and Gradle metadata; workstation caches, build outputs and credentials are not image inputs.
ciToolchainInputsCheck compares the evaluated Gradle projects and included convention-build sources with the Dockerfile's copy destinations, the context whitelist, and the hash inventory. It runs in preReleaseCheck through releaseCoreVerification, and before ciToolchainImageBuild, without building a container. When adding a module or convention source, update all three image inputs; the check reports omitted paths locally.
It pushes $CI_REGISTRY_IMAGE/stig-toolchain:$PINS_HASH and emits PINS_HASH as a dotenv artifact. The build jobs declare needs: build_toolchain_image and run on image: $CI_REGISTRY_IMAGE/stig-toolchain:$PINS_HASH, so they always get the image matching the current pins. kaniko layer-caching (--cache-repo …/stig-toolchain-cache) makes an unchanged rebuild a fast cache hit (a couple of minutes), so the job can run on every pipeline without penalty.
STIG_TOOLCHAIN_BASE_IMAGE pins Fedora 44 for the primary image. The Ubuntu image builder overrides that variable with STIG_GRADLE_CI_IMAGE, the existing Gradle/JDK Ubuntu base also used by lightweight release-preparation and changelog jobs. Both builders pass their selected base into ci/toolchain.Dockerfile as BASE and hash it, so their tags differ. The Ubuntu test job needs only the Ubuntu builder's dotenv; the primary jobs need only the Fedora builder's dotenv. The Dockerfile keeps native provisioning/build and Gradle metadata/prewarming in separate layers, so a Gradle verification-task edit need not rebuild PhASAR.
The local ciToolchainCheck computes the same Fedora input hash and runs ciBuildAndTest inside registry.gitlab.com/safetygence-oss/stig/stig/stig-toolchain:<PINS_HASH>. This explicit diagnostic task is implemented in gradle/ci-toolchain.gradle.kts; neither build nor preReleaseCheck schedules it. The normal host checks remain in place. The container check includes current unstaged and untracked source changes while keeping the original checkout/index unchanged; Gradle runs in a writable disposable container workspace. See source cleanup.
If a hash-input edit selects a tag that has not been published, prepare that exact image locally:
The build task uses the same Dockerfile and tag as CI without publishing. Podman builds use Docker image format so the Dockerfile's Bash SHELL and pipefail setting are retained. Version-banner checks consume all tool output while printing the first line, avoiding SIGPIPE when a producer writes more than the pipe buffer. On paths 2–3, scripts/submit.sh --local-container-checks on runs image preparation and then ciToolchainCheck before preReleaseCheck. New submissions default to off; agents opt in only on the user's request. Resume preserves the saved choice. See the submission option and resume policy. This does not change GitLab verification or release gates. For direct Gradle use both tasks remain explicit; verification never silently falls back to another image or skips for an unavailable runtime. For a deliberate diagnostic replay, ciToolchainTidyCheck -Pstig.ciToolchainImage=<existing-content-addressed-image> accepts a 12-hex input-hash tag or a SHA-256 image digest. An overridden run establishes results only for the selected image, not current Fedora CI parity. The log records both the selected image and the current input hash.
Set FORCE_REFRESH=1 (e.g. a monthly scheduled pipeline) to rebuild from scratch and pick up base-OS security updates.
Before Kaniko snapshots the final layer, the Docker build reports the retained PhASAR, Playwright, and Gradle-seed sizes, the approximate total uncompressed image filesystem size (excluding builder mounts), and the remaining filesystem capacity. These diagnostics make image growth and runner-space failures visible directly in the toolchain job log. Kaniko keeps cached layers uncompressed while building this large image (--compressed-caching=false), its documented low-memory mode; full-content filesystem snapshotting remains enabled. The image-builder job disables the repository Gradle cache because Kaniko resolves dependencies inside the image build; restoring that cache into the excluded project mount would consume runner disk without contributing to the image.
The PhASAR-only prebuild contract
The image runs build-analyzer.sh --no-build-analyzer --no-local-phasar to build only pinned PhASAR (the image carries no analyzer C++ source). Two invariants the script must honour on this path — both have bitten us, so don't regress them:
the C++-project check is deferred to the analyzer-build step (a prebuild has no
src/main/cppand must not abort),the script must exit 0 on success (no trailing
&&-short-circuit leaking a non-zero status).
Success records the pin, complete installed-file byte inventory and native toolchain identity in install/phasar. The per-pipeline build-analyzer.sh verifies all three and the installed CMake package before touching the source cache. A valid pinned install is copied into the pin-specific PhASAR directory and skips both the source/submodule checkout and the rebuild. A marker alone does not establish reuse. Do not move validation after source initialization: the image deletes the source tree intentionally, and recreating it consumes substantial job disk for no build benefit. Image creation and both native CI jobs run --verify-installation --no-build-analyzer --no-local-phasar as a read-only preflight; it cannot repair a bad install by cloning, building or installing native code.
Build and test
build_and_test runs on the Fedora 44 toolchain image (no setup.sh, no per-job package installation) and reads the image's dependency seed in place through GRADLE_RO_DEP_CACHE. Its project-local $GRADLE_USER_HOME contains additional downloads, metadata, cached task outputs and extracted transforms. GitLab retains the wrapper, extra dependency downloads and eligible task outputs as described under Cache reuse. Gradle recreates its version-specific extracted IDEA transforms in the job workspace. Compiled-model validation and Dokka use the complete runtime classpath to execute model code but reflect declarations only from explicit current-build output directories and project artifacts; project containment is not a model-output discovery rule, so a dependency cache beneath a consumer checkout cannot become model input. The job then runs ciBuildAndTest, the same aggregate used by the required local Fedora container gate. releaseCoreVerification is the aggregate core gate also used by preReleaseCheck; it owns the root and every subproject build, verifyThirdPartyNotices, and the Kover XML/HTML reports. Its builds already include native compiler checks and viewer ESLint/Stylelint. ciBuildAndTest additionally includes prepareWritersideSource and:
cleanupShellShellCheckfor ShellCheck;cleanupStructuredDataCheckfor strict JSON/YAML parsing, including.gitlab-ci.yml;:stig-gradle-plugin:cleanupMarkdownCheckfor structural Markdown checks; and:stig-code-analyzer:cleanupCppTidyCheckfor host-matched ClangTidy.
After dependency or PhASAR-pin changes, run ./gradlew generateThirdPartyNotices verifyThirdPartyNotices locally, or use ./gradlew cleanup to regenerate notices together with source cleanup. Include both generated notice files in the submission. Verification alone remains read-only and rejects stale files; when both generation and verification are scheduled, generation finishes before verification.
For a release request, ubuntu_compatibility_test runs releaseCoreVerification against the release candidate in the Ubuntu image. It does not run cleanup, formatting, ClangTidy, or publishing. create_release_tag needs this job as well as the primary verification jobs, so a failed Ubuntu compatibility test prevents release tagging. Ordinary branch and merge-request pipelines use the Fedora primary gate without this extra Ubuntu job.
All source formatting and formatting comparisons run only on developer machines through the local cleanup workflow. CI invokes neither cleanup nor cleanupCheck, and does not run IDEA formatting, ClangFormat, or shfmt checks. Developer-side preReleaseCheck still depends on the complete cleanupCheck; quickCheck also checks IDEA formatting. Test results and coverage are surfaced in the GitLab MR UI.
build_and_test first requires a nonblank STIG_RESTRICTED_KEYWORDS environment variable and runs checkRestrictedKeywords before the expensive verification tasks. releaseCoreVerification also includes this check. The checker reads newline-separated keywords and uses git ls-files --cached --others --exclude-standard to select tracked files plus unignored untracked files from the current working tree. It checks filenames and UTF-8 text, skipping binary or non-UTF-8 content, symlink targets, and deleted files. Matching ignores case and respects letter/digit token boundaries. Diagnostics contain locations and configured entry numbers, without matched text. Base64 packet data inside complete PGP public-key armor is treated as binary; armor headers, readable keyring metadata and surrounding text are still checked. Unterminated or nested public-key blocks fail with their location. Graphify artifacts (graphify-out directories and .graphify* files or directories at any depth) are excluded from both filename and content checks, even when tracked. Ignored build outputs are excluded; other generated files deliberately tracked by Git are still checked. The checker does not generate files or scan additional build directories.
The authored glossary is stig-docs/src/main/kotlin/com/stigway/cfg/glossary.xml. In addition to its raw XML text, the check examines decoded term names and descriptions, normalizing XML whitespace so entity escapes and line wrapping do not hide a restricted word or phrase. Raw and decoded matches for the same keyword within a term are not reported twice. Malformed glossary XML fails the check. Fix definitions or examples in the authored XML; generateWritersideGlossary produces the Markdown page under stig-docs/build/generated/writerside/docs/glossary.md.
build_and_test runs prepareWritersideSource and publishes stig-docs/build/writerside-source/ as an artifact. This task copies the authored glossary XML unchanged, includes the generated Markdown and references, and stages the documentation media. build_docs_writerside consumes that prepared module and verifies both glossary files are present before invoking the pinned Writerside builder. Local and CI documentation builds therefore use the same prepared sources.
Keyword matches are an expected verification failure: the checker prints all locations and exits with status 1, without an uncaught JVM exception. Gradle marks the task failed; --stacktrace still requests Gradle's own failure trace. This repository check is independent of the consumer analysis CRITICAL/failAt mechanism.
Keep the keyword list outside the repository, in a GitLab CI variable and optionally the local shell environment. Local checks skip an absent or blank list. CI fails when the list is absent or blank, so configure variable visibility for every branch, tag, and merge-request pipeline that runs this gate. A protected-only variable is unavailable to unprotected branches and many merge-request pipelines; those pipelines cannot pass this gate without the variable.
Analyzer native regressions — every source-build execution of
:stig-code-analyzer:stigBuildAnalyzerenables CMakeBUILD_TESTING, builds the GoogleTest cases, and runs their CTest registrations with failure output. This includes STIG's focused regression for PhASAR's exact access-path lifetime identity and canonical fact ordering. The earlier toolchain-image step builds only the reusable PhASAR install; the native cases run later whenbuild_and_testlinks the analyzer.PhASAR/analyzer integration —
buildruns:stig-gradle-plugin:integrationTest, including the production analyzer matrix, its accuracy metrics, and the focused STIG regression cases. The toolchain-image build separately compiles PhASAR's LLVM fixtures and runs every registered CTest executable before installing the pinned dependency. The build job reuses that tested installation; source builds outside the image follow the same PhASAR test gate. See testing overview for disabled cases and the exact native and Kotlin test scopes.Gradle plugin behavior —
:stig-gradle-plugin:checkalso depends onfunctionalTest, so TestKit plugin behavior and the real-analyzerintegrationTestsuite run alongside the ordinary unit tests. Functional-test failures print full exceptions and their causes, including the nested TestKit build failure, in the job log.Viewer rendering —
buildruns:stig-gradle-plugin:check, which depends onstigViewerChecks, including the Playwright:stig-gradle-plugin:stigViewerRenderTesttask. This is the same viewer test family reached by the localpreReleaseCheckgate.IDEA PSI inspections —
buildruns:stig-idea-plugin:test, including IntelliJ light-fixture tests that materialize Kotlin PSI into STIG model entities and verify generated rule inspections/quick fixes.JUnit — collected across every module from
**/build/test-results/{test,integrationTest,functionalTest,modelEditorUiTest}/TEST-*.xml, so unit, integration, functional, and model editor UI suites all show up. GitLab reads the original Gradle XML outputs even when the build fails.Coverage — Kover's JaCoCo-format
report.xml, plus the HTML report as a browsable artifact (ci-reports/).
The root STIG build suppresses test stdout/stderr in the INFO-level console log for every subproject Test task, including output forwarded by nested TestKit builds. CI retains --info build diagnostics and test failure exceptions; complete captured test streams remain in the JUnit XML and HTML test reports. This changes console presentation only, not test execution, failure status, or report capture. INFO is configured explicitly because ordinary testLogging settings apply to LIFECYCLE and do not suppress INFO-level standard-stream events.
The job's after_script collects the available HTML and XML reports into ci-reports/ after a successful or failed build. JUnit and coverage uploads use the original Gradle outputs directly, so they do not depend on report copying succeeding. A successful build still requires JUnit results and the Kover XML report.
The PhASAR fork has a separate branch pipeline for stig-patches-2026.06. It configures PHASAR_BUILD_UNITTESTS=ON and PHASAR_BUILD_IR=ON, builds the upstream test executables and their LLVM fixtures, runs the check-phasar-unittests CMake target, and installs only after CTest succeeds. XTaint13 and XTaint14 remain in the executable as explicitly disabled GoogleTest cases because their sanitizer expectations have known pre-existing false positives; disabling those two cases does not suppress the rest of IDEExtendedTaintAnalysisTest.
That branch pipeline validates pushed PhASAR commits; it is not run by an uncommitted sibling override. STIG releases still select one exact PHASAR_COMMIT from that branch in stig-code-analyzer/scripts/pins.env, while a local sibling checkout deliberately overrides the pin during development.
verify_idea_plugin is a separate build-stage job. It runs ./gradlew ideaPluginVerification, which explicitly runs :stig-idea-plugin:test, builds the unsigned IntelliJ plugin archive, checks the plugin XML/archive structure, validates the plugin project configuration, and runs JetBrains Plugin Verifier against the same resolved pinned IDEA installation used by the module's compile/test setup. Keeping this as a separate job gives the verifier its own logs and avoids mixing large IDE-resolution output into the main test/coverage job. The plugin build also runs generateStigIdeaInspectionMetadata, so the verifier checks the packaged per-rule inspections and generated inspection-description resources rather than a hand-maintained inspection list.
Cache reuse
default.cache stores the Gradle wrapper, dependency/JAR caches (modules-2 and jars-9) and cacheable task outputs (build-cache-1) below .gradle/. Its key combines the project ID and the content of gradle/wrapper/gradle-wrapper.properties. Job-specific cache definitions replace this default.
Jobs | GitLab cache behavior |
|---|---|
| Inherit the shared |
| Restore/save three archives: wrapper distributions, job-specific dependency downloads, and job/toolchain-specific task outputs. |
| Reuse the wrapper archive and restore |
| Skip the unused Gradle archive; required inputs continue to arrive as artifacts. |
| Skip the repository Gradle archive and use Kaniko's separate registry layer cache, except during an explicit |
Jobs running in either toolchain image read the baked dependency seed in place through GRADLE_RO_DEP_CACHE. Gradle downloads dependencies missing from that seed into writable modules-2; a separate GitLab archive retains those downloads without copying the seed. Whole Gradle homes, configuration-cache state, and version-specific extracted IDEA transform directories are not included. Configuration-cache state stays local to the job; it can contain sensitive configuration values.
All three keys include the wrapper file's content hash. Their prefixes and ownership are:
Archive | Key prefix | Writers/readers |
|---|---|---|
|
| Shared by toolchain jobs |
|
| Each validation job writes its own archive; release reads |
|
| Each validation job writes its own archive; release reads |
A toolchain-image change invalidates the task-output archive, not ordinary Maven downloads. Separate job names avoid parallel Fedora, Ubuntu, IDEA and Dokka jobs overwriting each other's dependency/task archives. Release reads the tested build's archives after that job succeeds, without publishing its new dependency/task entries back.
Matching archives can be reused in later pipelines, subject to GitLab's cache availability and protection rules. Pipelines running the same job can still update the same archive; GitLab cache restoration is best-effort, not an artifact-delivery guarantee. Preserve GitLab's protected/unprotected cache separation: untrusted pipelines must not write a trusted release cache. No remote Gradle build-cache service or credentials are introduced here. org.gradle.caching=true enables reuse of outputs for cacheable tasks whose inputs match, including after clean; it does not make every task cacheable or add a native compiler cache. Compare cache-transfer durations, archive sizes and Gradle's FROM-CACHE results with total job time to judge the speed benefit.
Job timeouts
GitLab's timeout applies to the total runtime of each CI job. All commands in a job share its time budget, including before_script, the Gradle invocation, report collection, and after_script. The Gradle BUILD SUCCESSFUL in ... duration covers only that Gradle invocation. Other jobs have independent limits, so the full pipeline can take longer than one job's timeout.
This CI configuration inherits the project's timeout from Settings > CI/CD > General pipelines > Timeout; GitLab's default is 60 minutes. To give only build_and_test more time, add a job-level override to its existing definition in .gitlab-ci.yml, for example:
A job-level override can exceed the project default, but the runner's maximum job timeout still caps it. The job variables RUNNER_SCRIPT_TIMEOUT and RUNNER_AFTER_SCRIPT_TIMEOUT can reserve time for final diagnostics and artifact uploads by ending scripts earlier. They cannot extend the overall job timeout; after_script defaults to five minutes and is also limited by the job's remaining time. See GitLab's job timeout setting and script timeout controls.
Documentation and Pages
Two independent doc builds feed one site:
build_docs_dokka—./gradlew dokka→ the API reference (stig-docs/build/dokka/).build_docs_writerside— the JetBrains Writerside builder renders the user guide. It receives the preparedstig-docs/build/writerside-source/artifact frombuild_and_test, copies it into a clean temporary source root, and builds productstig/stig.
Before artifact upload, build_and_test checks that the prepared Writerside configuration and glossary files are nonempty, build_docs_dokka checks its index.html and version.json, and build_docs_writerside requires a nonempty output ZIP. The Pages job also validates ZIP contents and the assembled site.
build_docs_dokka restores the previous Pages artifact under build/documentation-versions, moves its unpacked public directory into previous-public, and removes the downloaded ZIP. The Pages job consumes that downloaded previous-public input by moving it into public before site assembly. These moves reduce temporary duplication; the copies required for the stable, snapshot and exact-version URLs remain.
The pages job runs only for the default branch. It unzips the Writerside output as the site root and copies the Dokka output under public/dokka/. Merge-request and tag pipelines can validate documentation without replacing the public site. Only what's in stig.tree is published; the workplan/ notes live outside the Writerside topics dir on purpose, so they stay repo-only.
writerside.cfg sets the media directory's web-path to .. Writerside therefore emits page-relative URLs for screenshots, video, posters, favicons, and dark-theme variants. The built archive works both at a site root and below GitLab Pages' /stig/stig/ path; the Pages job copies it without rewriting generated HTML. When checking media, serve the generated site below a non-root path and check both themes, because root-relative URLs can appear to work in a root-only preview.
Documentation version channels
This section describes STIG's own Writerside/Dokka documentation website. Consumer reports published with stigPublishHtml follow the separate report version and URL rules.
For a short URL guide, see Documentation versions and links.
Maven and the JVM ecosystem do not prescribe documentation URL paths. They do establish the version semantics that STIG applies to its documentation: an X.Y.Z release is immutable, while X.Y.Z-SNAPSHOT is a replaceable development coordinate. Maven repositories may implement one logical snapshot with timestamped physical artifacts and changing metadata; STIG Pages implements the same user-visible mutability by replacing the content of one snapshot channel. Maven metadata's latest value can identify the newest deployed version, including a snapshot, while its release value identifies the newest non-snapshot. The historical LATEST and RELEASE dynamic dependency versions are not a documentation convention and explicit dependency versions are preferred. STIG therefore uses the site root as a stable release channel, keeps the snapshot separate, and does not publish an ambiguous /latest/ channel.
STIG maps those semantics to these Writerside and Dokka URLs:
/and/dokka/contain a physical copy of the newest released version. They are normal200content locations, not symlinks or HTTP redirects./snapshot/and/dokka/snapshot/contain the newest successful non-release build frommainand may be replaced./versions/X.Y.Z/and/dokka/versions/X.Y.Z/contain the retained release at an immutable, exact-version URL. The newest release is available both there and at the stable roots.Before the first release exists, the site roots duplicate the snapshot as a bootstrap fallback. Once any release exists, snapshot builds leave the roots on that release.
Publishing X.Y.Z does not overwrite or transform X.Y.Z-SNAPSHOT; Maven treats them as different coordinates. The release publishes X.Y.Z at both the stable roots and its exact-version paths. The last successful snapshot remains available whether it was built before or after that release, until the next successful snapshot build replaces it.
Serving the stable root directly follows Gradle's /current/ documentation pattern. Gradle returns the current pages as 200 content and declares their stable URLs as canonical; it does not redirect each request to a numbered release. STIG uses / for that stable reader-facing location and keeps /versions/X.Y.Z/ as the exact-version permalink. The Writerside build enables search indexing, emits canonical links rooted at /, and generates the root sitemap. Copies under /versions/ therefore point crawlers back to the stable URL instead of competing with it. Writerside's fixed index.html redirect to its configured start page is renderer-generated and does not change target when STIG releases.
The root build.gradle.kts setting maximumRetainedDocumentationReleases is one. The site therefore keeps the newest release at the stable roots and at its exact-version paths, plus the independently replaceable snapshot channel. Gradle prunes older restored Writerside and Dokka inputs before rendering and writes the canonical retained-release manifest. The pages job uses that manifest to remove expired exact-version directories from the restored site instead of merely hiding them from the version switcher. Change the integer when the capacity policy changes; a separate enum is not needed.
GitLab.com's instance configuration currently limits one Pages deployment and one job artifact to 1000 MiB. For a clean 2026-09-07 build, the published files totalled about 43.74 MiB for Writerside and 111.14 MiB for Dokka: 154.88 MiB for one complete documentation version. At that size, the stable-root copy, exact-version copy, and snapshot use approximately 464.6 MiB, leaving about 535.4 MiB before the Pages limit. Recheck this budget when either renderer or the documentation set grows materially.
For example, the available URLs evolve as follows:
Stage | Stable URLs: | Snapshot URLs: | Exact release URLs |
|---|---|---|---|
First |
|
| none |
Release |
| last successful |
|
Next | remains |
| remains |
Release |
| last successful |
|
Next | remains |
| remains |
The pages job runs only for the default branch. Any successful non-release main pipeline updates the snapshot channels, regardless of whether the commit arrived through submit Path 0, 1, or 2. A Path 3 or 4 release-trigger commit replaces the stable-root copies, creates the new exact-version copies, and removes the preceding release. The release job fails rather than replacing an existing exact-version directory for the same release.
Writerside reads the shared versions.json. It marks the newest release as current and links the development build as X.Y.Z-SNAPSHOT; the snapshot is current only during the no-release bootstrap. Dokka uses its first-party versioning plugin for the stable release and retained history. That plugin derives destinations from version names and cannot map a displayed X.Y.Z-SNAPSHOT independently to the fixed /dokka/snapshot/ path, so the snapshot API reference omits the cross-channel version selector and embeds no historical copies. Use the Writerside version selector to change channels. The canonical Dokka channel URLs above remain stable.
Release preparation pushes its generated changelog commit with GitLab's ci.skip push option: the initiating pipeline already checks out that exact commit for every validation and publication job, while skipping a redundant branch pipeline prevents two default-branch Pages jobs from publishing the same release concurrently. The tag push still creates its normal tag pipeline.
Checking the docs locally (before you push)
Prefer the container Gradle tasks — even if you also have a local Writerside / IntelliJ builder. They pin the exact builder CI uses (writerside-builder:2025.11.8530), so a local pass matches CI byte-for-byte; a host builder's plugin version drifts from that pin and can accept/reject things CI won't. They need only podman/docker:
Task | What it does |
|---|---|
| build the docs in the container and serve them at |
| build the merged site (user guide + API ref) into |
| the API reference only |
| check repository-wide IDEA formatting, compile every subproject's main source set, and type/syntax-check the Revideo tour without rendering; omit tests, assemble, mutating cleanup, Writerside, and plugin-verifier gates |
| render and verify the IntelliJ IDEA tour MP4 and extract its Writerside poster |
| build the tour when needed and stage canonical Writerside source plus generated media in |
| run the shared local/CI core gate: root and all subproject builds/tests (including the analyzer integration matrix), notice verification, and Kover XML/HTML reports |
| run the read-only repository formatter, parser, linter, compiler-warning, and C/C++ static-analysis gate |
| run the shared core and cleanup gates plus aggregate Dokka, Writerside container inspection, IDEA PSI tests, unsigned IDEA-plugin archive/structure/project/verifier checks, and Qodana; CI toolchain container tasks are separate |
| the Dokka output for the |
| run only the pinned Writerside container inspection/build, with the local Podman flags used by STIG developers |
writersideContainer runs Writerside's inspections (CDE/REF/…) and fails on errors (unknown code-fence language, broken anchors, …) — so running it before pushing catches what the CI build_docs_writerside job would. The non-container writerside/writersideServe tasks need idea.sh on the PATH (WRITERSIDE_IDEA_SH) and are only for environments that already have the builder installed; they aren't the normal local path.
The tasks are intentionally separate:
scripts/generate-dokka-in-podman-local.shis a convenience wrapper around./gradlew --no-configuration-cache writersideContainerBuild. It validates the Writerside source tree in the pinned container and writesstig-docs/build/writerside-output; it does not run:stig-docs:dokkaGenerateHtmlor the aggregatedokkatask../gradlew :stig-docs:dokkaGenerateHtml --console=plainis a scoped Dokka task for thestig-docsGradle module. It does not invoke the Writerside container script and does not checkstig.tree, Writerside links, or Writerside inspections../gradlew writersideContaineris the full local site build. It depends on the Writerside container output and on the aggregatedokkatask, then merges them into./public.
The local container build first runs prepareWritersideSource, then copies that staged STIG Writerside module into a temporary source root and drops .idea before invoking the builder. Local IDE state is not documentation input; keeping it out of the builder prevents machine-specific IntelliJ settings and rootless-container permissions from affecting the docs build. CI performs the equivalent staging from the build_and_test media artifact because the Writerside builder image does not contain the Node/Revideo toolchain.
Releasing
Versioning is Axion (the version is derived from vX.Y.Z git tags — no manual version bumps in files) plus the JetBrains Changelog plugin for CHANGELOG.md.
Axion is the single version authority for every STIG source build, including when stig is an included composite build. An exact v1.9.0 checkout is version 1.9.0; commits after that tag are 1.9.1-SNAPSHOT. The root project.version is propagated unchanged to every subproject, the Gradle-plugin and analyzer JAR metadata, the bundled Codex plugin manifest, and the native analyzer's STIG_VERSION compile definition. At runtime getStigVersion() reads that packaged metadata and fails on missing, placeholder, or conflicting values. It does not independently reinterpret Git history or use CHANGELOG.md as a version fallback.
The checkout must therefore own its .git metadata before a versioned artifact, publication, or analyzer build can run. Those operations depend on the strict requireStigVersion gate. Tasks that do not produce or execute a versioned STIG artifact, such as the CI toolchain image's IntelliJ dependency-cache prewarm, do not request the version and can operate on the Dockerfile's intentionally narrow metadata copy.
Prepare the changelog before a release
Put your notes under ## [Unreleased] in the repo-root CHANGELOG.md. Follow the Changelog writing policy in Change sync instruction: select notable effects on STIG users, describe the final behavior and required actions concisely, and consolidate related changes.
Every released section must contain release notes. verifyChangelog rejects empty released sections, and patchChangelog refuses to create or retain an empty release section before writing the file. Empty category headings, HTML comments, and comparison-link definitions do not count as notes. A retry may leave [Unreleased] empty when the planned version already has notes; existing notes and the release date are retained. For a release containing only internal maintenance, write a brief maintenance note under a standard changelog category.
The IDEA plugin includes its matching version's notes and a link to the full public release history, including STIG versions that were not published to JetBrains Marketplace.
The changelog remains release documentation and release-gate input; it is not embedded in the native analyzer and does not determine the analyzer or plugin version.
Before a release trigger
Before setting release=incrementPatch/incrementMinor/incrementMajor, or before committing an increment* marker, run the full local pre-release gate:
The gate retains host, documentation, IDEA-plugin, and Qodana verification; the Fedora CI toolchain container check is separate and opt-in through submit.sh --local-container-checks on on paths 2–3. Direct diagnostic use runs ciToolchainImageBuild then ciToolchainCheck. Linux defaults to Podman; use -Pstig.ciToolchainContainerRuntime=docker for those tasks when Docker is the local runtime. An explicitly requested check fails if the runtime or image is missing; no image push is required. Disabling this optional CI reproduction does not disable documentation/Qodana containers or GitLab release gates.
On Linux, scripts/submit.sh automatically runs preReleaseCheck in a private 1920×1080 Xvfb display, for both interactive submissions and numbered paths 2–3. This supports Wayland and headless hosts and requires xvfb-run, Xvfb, and Xauthority. The runner stops if display startup or verification fails. A direct Gradle invocation outside the submission script still needs a working X11 display or an xvfb-run wrapper.
On Linux, the Writerside container task defaults to Podman with SELinux relabeling and the local container limits needed by STIG developers. Override writerside.containerRuntime, writerside.volumeFlags, or writerside.containerRuntimeArgs only when the local runtime needs different settings.
Use ./gradlew :stig-code-analyzer:stigBuildAnalyzer --rerun-tasks when you need to force the analyzer relink instead of accepting a Gradle cache hit. Use ./gradlew writersideContainer when you also want the merged local site in ./public.
Trigger a release (two equivalent ways)
CI variable — run a manual pipeline on
mainwithrelease=incrementPatch(orincrementMinor/incrementMajor).Commit marker — include
incrementPatch/incrementMinor/incrementMajorin the commit title or message onmain. (With squash merges, the marker must be in the squash commit.)
incrementPatch, incrementMinor, and incrementMajor are not normal commit messages. They are release triggers; use them only when you intentionally want CI to cut a release.
What the release jobs do
prepare_release_commitruns an Axion dry-run to compute the targetvX.Y.Z. The dry-run result is treated as metadata only: any local tag left behind by the dry-run is deleted before the release-notes commit is pushed.verifyChangelog, thenpatchChangelog(promote[Unreleased]→ the new version) andgenerateWritersideReleaseNotes(regenerate therelease-notes.mdtopic fromCHANGELOG.md), thenverifyChangelogagain. If an earlier untagged release attempt already created the planned version section,patchChangelogretains that section's date and merges new[Unreleased]entries into it instead of creating a duplicate header or dropping the new entries.prepare_release_commitcommitsCHANGELOG.md+release-notes.md, pushes that commit tomainusing an explicit branch refspec with tag-following disabled, verifies that the planned tag is still absent onorigin, and exportsRELEASE_TAG/RELEASE_VERSION/RELEASE_COMMIT_SHAas a dotenv artifact.build_and_test,verify_idea_plugin,build_docs_dokka,build_docs_writerside, andverify_changelogcheck outRELEASE_COMMIT_SHAand validate that exact release commit.create_release_tagrunsrelease --pushTagsOnlyonly after those validation jobs pass. Permanent tags are created only in this job. It uses the prepared version and validated commit even ifmainadvances during verification. The job checks that this commit remains in the remote branch history and rejects an existing tag for another commit. It disables only Axion's remote-position check; the working-tree and dependency checks remain enabled.pagesandrelease_and_publishconsume the same validated release metadata.After
release_and_publishsucceeds,trigger_update_user_projectsrunsscripts/trigger-update-user-projects.sh. It dispatches each configured project's own pipeline with typed inputsupdate_project=trueandrelease_version=RELEASE_VERSION. It does not clone, update, build, or publish consumers. The job finishes when GitLab accepts the requests; it does not wait for consumer results. Rejected requests are reported, remaining projects are still attempted, and a rejected dispatch fails this job. Retry dispatch without republishing the already uploaded STIG release. The publication-recovery path dispatches after its publication too.Each consumer owns its
update_projectjob. Shared definitions in.gitlab/ci/user-project.ymlrunstigUpdate --yeswith its default latest stable release policy, commit its dependency/version changes, and push with that consumer's own job token. Model projects selectstigVersion; ordinary applications and libraries usestigUpdateProfile=applicationand selectstigToolchainVersion. Application updates complete the declaredstigApplicationCheckTasksin a fresh strict process before the job commits or pushes. The public examples then runbuildandstigGenerateHtmland deploy Pages in the same job and updated checkout. Other consumers can leave build or HTML publication disabled. Their failures remain visible in their own pipelines and do not change the STIG pipeline result. Ordinary public-example pipelines still build and publish their saved version without updating it.
After the tag is created, release_and_publish publishes that exact version from the validated release commit (see Publishing for the required signing keys and CI variables). Before any upload, verifyPublicationSignatures verifies all Maven and Gradle Plugin Portal artifacts against the checked-in public release key and its full fingerprint. Direct publication tasks have the same verification dependency. The toolchain image includes GnuPG for these checks; the key, policy, and build convention participate in its content-derived hash. For the IDEA plugin, verify_idea_plugin has already checked the unsigned archive and verifier compatibility; the publish job signs the archive, verifies the signature, and the publish task is hard-wired to upload the verified *-signed.zip; an unsigned fallback fails before Marketplace upload. The Marketplace token variable checked by this job is PUBLISH_TOKEN; ORG_GRADLE_PROJECT_intellijPlatformPublishingToken is redundant unless the IDEA-plugin build is changed back to the IntelliJ Gradle plugin's default token convention.
Configure the dispatch list as a protected, masked STIG CI/CD variable named STIG_USER_PROJECTS containing a single-line JSON array: [{"id":123,"ref":"main","token":"PROJECT_TRIGGER_TOKEN"}]. Create one pipeline trigger token in each participating project. Keep private identities and all credentials out of committed source. The dispatcher uses project trigger tokens rather than STIG's job token: these pipelines are independent, so private project names are not exposed as downstream cards in STIG's public pipeline graph. It logs only configuration positions and HTTP results, not tokens or project URLs. Typed pipeline inputs avoid enabling arbitrary pipeline-variable overrides.
Create safetygence-oss/stig/stig-robot as a blank public project, then populate main with a fresh copy of stig-project using the project-copy instructions. Keep the 0BSD license for this public example, give it its own project name and README, and keep the not-ready notice until the robot model is usable. Use stigInitElement for its new model elements. An empty repository or README alone cannot run stigUpdate: commit the Gradle build, wrapper, updater JAR, setup script, and dependency policy from the template first. Keep stigSource=published so release CI does not depend on sibling source checkouts.
In each consumer, enable Allow Git push requests to the repository for its own CI job token and allow the trigger token's owner to push the default branch. Cross-project Git push access is no longer needed. Job-token pushes do not start another pipeline. Public examples build and publish from the updated checkout in the maintenance job. The model updater switches stigSource=local to published; CI checks the published selection after the update. An inactive local-mode stigVersion does not constrain release selection. The application profile does not require model-project properties. Both profiles authenticate the release, refresh wrapper/locks/checksums, and verify dependencies. The job requires at least the requested release before committing and always runs the updater's default release selection, including when that minimum was already satisfied. It rejects downgrades of an active published version, accepts managed root and included-build gradle/stig.versions.toml files, rejects unexpected new files, and does not create empty commits. A resource group serializes updates inside each project. Register libraries and applications in the same protected dispatch list as model projects; their update pipeline must accept the same typed inputs. Normal pipelines use their committed dependency policy without running an update.
Applications with an authored npm workspace can include its Gradle update task in stigApplicationUpdateTasks and its locked install/build task in stigApplicationCheckTasks. The checked-in updater JAR carries the shared META-INF/ai.exida.stig/update/update-js-vendor-deps.mjs adapter, also used by STIG's source updater. Invoke it with the managed Node binary, workspace path and updateMode (major by default). An application that runs npm with install scripts disabled passes --no-install-script-approvals; STIG's source workspace keeps its reviewed esbuild/puppeteer approvals. The adapter selects stable exact pins compatible with that Node and direct peers; the task regenerates package-lock.json with npm and subsequent checks use npm ci. The update transaction restores authored package.json and package-lock.json files if a check fails. Shared libraries maintain their npm workspace once; consuming applications use the packaged lock without independently changing its pins.
Applications that deliver through their normal pipeline set STIG_UPDATE_DELIVERY=true. After checks and a successful push, the shared job uses its own job token to start that project's default-branch pipeline with the typed input delivery_commit set to the checked commit SHA. This also happens when a retry finds no changes, so a failed dispatch can be retried without an empty commit. The consumer must declare that optional input (empty by default, otherwise exactly 40 lowercase hexadecimal characters), expose it as STIG_DELIVERY_COMMIT, and reject a nonempty value unequal to CI_COMMIT_SHA in workflow:rules. This prevents a concurrent branch change from delivering an unchecked commit. The request omits update_project, so its false default selects normal verification and delivery rather than another update. The shared job validates the API response SHA, project and branch and saves it under build/stig-update/delivery-pipeline.json; dispatch failure fails maintenance. Delivery results remain in the consumer's linked pipeline. A library can opt in for normal verification while reserving publication for protected, version-matching release tags; maintenance never republishes an existing library version.
A private library can trigger its consumers after publication using a separate typed release-version input. Keep that library's version separate from release_version, which always means STIG. The consuming job can set STIG_UPDATE_CATALOG_VERSION_KEY to the library's key in gradle/libs.versions.toml and STIG_UPDATE_CATALOG_MINIMUM_VERSION to the requested exact x.y.z release. The shared job runs the updater's default release and catalog update and checks that this pin meets the minimum before pushing; it fails if the publication is not yet resolvable. For consumers with a local/published source selector, STIG_UPDATE_SOURCE_PROPERTY names that Gradle project property. The job passes an explicit -P<property>=published, which takes precedence over a checked-in local selection and is forwarded to updater workers. No source checkout is inferred from sibling directories. The private publisher owns its trigger list and credentials; no private identities or library policy belong in the public STIG repository.
The update job installs GnuPG for authenticated release verification and curl/jq for delivery dispatch; it does not install native analysis tools or fetch source repositories. Normal build/Pages jobs do not need that update-only prerequisite. Retry a failed consumer job in that project; retrying the STIG dispatcher starts fresh consumer pipelines without republishing STIG. See GitLab's trigger token API and job-token Git push settings.
Diagnose publication without repeating the full pipeline
A failed release_and_publish job can be retried alone in GitLab. Its retry uses the original RELEASE_COMMIT_SHA and job configuration, so it is appropriate for a corrected CI credential or a temporary service failure. Pushing a source fix does not change that earlier job's checkout.
For publication code or dependency-policy repairs, run a pipeline on the default branch with STIG_PUBLICATION_CHECK=true and STIG_PUBLICATION_CHECK_VERSION set to the next unpublished x.y.z version. Only build_toolchain_image and publication_check run. The image job reuses the existing content-derived image; the check inherits the release job's signing setup and tested-build caches. It builds and verifies all publication signatures, including the verification bundle and signed IDEA plugin, then resolves the three upload task graphs with --dry-run. --continue collects independent signing failures in the same attempt.
This mode uses a job-local version tag and performs no tests, uploads, release preparation, remote tag creation, Pages deployment, or consumer repository updates. It validates packaging and signing, but cannot establish that external services will accept an upload.
After the full release validation has already passed, a maintainer repairing publication can explicitly authorize publication without repeating those checks. Set STIG_PUBLICATION_RELEASE=true and STIG_PUBLICATION_RELEASE_VERSION to the next unpublished patch version on the default branch. Do not also set STIG_PUBLICATION_CHECK; the two modes are mutually exclusive. Release preparation checks that Axion selects the requested version before updating and committing release notes. The pipeline then runs build_toolchain_image, publication_check on that prepared commit, publication_release_tag, and publication_release. The final job verifies signatures again and uploads to Maven Central, the Gradle Plugin Portal, and JetBrains Marketplace, in that order. It skips its redundant clean build. Only STIG receives a release commit and tag; Pages are excluded from this mode. After publication succeeds, trigger_update_user_projects requests the same consumer updates as a normal release.
Use this recovery mode only when the earlier successful build, tests, and compatibility checks remain applicable to the reviewed changes. It does not replace validation for new product changes. For those, use the normal release pipeline.
Don't release a PhASAR you didn't test — the sync guard
A release builds the pinned PhASAR (PHASAR_COMMIT in pins.env), never your local sibling checkout. If you develop against a local phasar/ sibling and it's dirty, off-pin, or unpushed, a release would ship a different engine than you validated. scripts/check-phasar-sync.sh guards this:
build-analyzer.shruns it in warn mode when building against a local PhASAR sibling,a
commit-msghook (auto-installed on dev machines) runs it--strictand refuses anincrementMajor/Minor/Patchcommit while PhASAR is out of sync.
Full rules: Local multi-repo development.
See also
Publishing — Maven Central / Gradle Portal / JetBrains keys + CI variables.
LLVM toolchain — what
setup.shprovisions and why LLVM 18.Local multi-repo development — explicit published/pinned or local working-tree selection.
.gitlab-ci.yml,ci/toolchain.Dockerfile,stig-code-analyzer/scripts/{pins.env,setup.sh,build-analyzer.sh}andscripts/{check-phasar-sync.sh,update-phasar-pin.sh}.