> Documentation version: 3. [HTML page](optional-workflows.html)

<a id="optional-workflows"></a>

# Optional workflows

Some repositories need more than the three core workflows. Each addition below
has a fixed name and purpose, so that the same file means the same thing
everywhere. All of them follow the rules in
[The three workflows](workflows.md#the-three-workflows): full-SHA
references, ` permissions: {} ` at the top, and no repository code in a job that
can write.

| Workflow | Trigger | Purpose | Token access |
| --- | --- | --- | --- |
| ` docs.yml ` | Push to the default branch; pull request | Build the site and manual; deploy to GitHub Pages from the default branch. | Build ` contents: read `; deploy ` pages: write `, ` id-token: write ` |
| ` fuzz.yml ` | Schedule; manual dispatch | Run fuzz targets for a bounded time and keep reproducers. | ` contents: read ` |
| ` bench.yml ` | Schedule; manual dispatch | Measure performance against a recorded baseline. | ` contents: read ` |
| ` codeql.yml `, ` scorecard.yml ` | Push, pull request, schedule | Static analysis and repository-practice scoring. | As each tool documents |
| ` link-inputs.yml ` | Pull request touching producer inputs; manual dispatch | Build, test and attest native linker inputs. | ` contents: read `, ` attestations: write `, ` id-token: write ` |
| ` publish-link-inputs.yml ` | Manual dispatch from the default branch | Publish an attested candidate and commit its lock. | See [Trusted publisher contract](linker-input-releases.md#trusted-publisher-contract) |

<a id="risk-campaign-separation"></a>

## Risks and controls for sustained campaigns

A separate campaign job can still run on every PR, and a cancelled search can
look like a successful time-bounded run. Separate workload intent and evidence.
Use explicit schedule/manual event guards, normal bounded search completion,
per-input limits and an overall safety timeout. Preserve reproducers and exact
compiler/source identities on failure. Check the actual triggers and reproduce
a saved failing input. Campaign results describe the selected search; neither
their success nor a short PR smoke run substitutes for published-release
compatibility or a meaningful benchmark measurement.

<a id="documentation-site"></a>

## Documentation site

` docs.yml ` has two jobs.

The build job checks out the repository, builds the site, and uploads it as a
Pages artifact. A repository with an AsciiDoc manual uses
[` actions/build-docs `](actions.md#build-docs), which also produces the PDF.
A pull request runs this job, so a broken manual fails review, and the artifact
can be downloaded to preview the change.

The deploy job runs only for the default branch. It has ` pages: write ` and
` id-token: write `, uses the ` github-pages ` environment, checks out nothing, and
deploys the artifact the build job produced.

<a id="what-the-site-points-at"></a>

### What the site points at

A visitor to the site is almost always a user, and a user has the latest
release. The site therefore leads with the latest release, not with the default
branch:

- its primary API reference is the ` roc docs ` output generated for the latest
  release, restored from that release’s documentation archive;

- its examples link is the examples archive attached to the latest release; and


- its install snippet names the latest release’s bundle URL.


"Latest" excludes prereleases; see
[Release candidates](workflows.md#release-candidates).

The site may also carry the default branch’s documentation, under a path and a
label that mark it as unreleased, and the documentation of earlier releases,
each restored from its own archive. Nothing rendered is committed: the site is
assembled at build time from the working tree and from release assets.

A new release changes what "latest" means, so ` release.yml ` rebuilds and
deploys the site after it publishes, in a job that holds only the Pages
permissions and deploys an artifact built by a read-only job.

<a id="fuzzing"></a>

## Fuzzing

Fuzz targets are written with roc-fuzz; see
[Correctness and memory properties with roc-fuzz](testing.md#property-based-tests-with-roc-fuzz)
for why and how. Named regression examples, memory assertions and accepted
allocation baselines run in fast PR checks. This workflow adds sustained
exploration; roc-fuzz’s default 60-second ` run ` is not a PR smoke command.

` fuzz.yml ` runs sustained campaigns on a schedule or manual dispatch. It has no
pull-request trigger, and campaign jobs explicitly accept only those two event
types on the default branch:

``` yaml
if: >-
  (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') &&
  github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
```

The same guard is required if a campaign job shares a workflow file with PR
jobs. A separate job or runner alone does not keep expensive work out of PR CI.
Campaign jobs have ` contents: read `, disable persisted checkout credentials and
run no publishing or pin-update controller.

Resolve the latest Roc nightly once and record its exact tag and ` roc version `,
the tested source commit, platform release/digest, target, arguments and duration
in the report. Define whether a budget applies per target or to the complete
campaign; ten minutes per target across dozens of targets is not a ten-minute
job. Bound each input’s time and memory as well as the campaign’s search time,
and leave headroom in the job timeout for building, cleanup and uploading logs.

Use ` TARGET run CORPUS --report-dir=PATH ` to check declared examples and exact
baselines before exploration, and write one supervised report. Choose a fresh
report directory for each invocation. Baseline failures skip exploration;
verification never invokes ` accept `. Missing baseline entries are ` unbaselined `,
so review and commit the intended baseline inventory before relying on it.

Restore and save the evolving corpus through the Actions cache, separately
from the small committed PR smoke corpus. Cache absence starts a new search;
it never excuses a missing target. Preserve crash inputs, raw stdout/stderr and
the run metadata as artifacts even when testing fails. Reproduce findings with
the saved input, not the random seed alone. A property failure, crash or
per-input timeout, allocation assertion or baseline change fails the job. A
campaign that reaches its configured search limit normally may pass. Zero
accepted exploration inputs are inconclusive and unsuccessful. Job cancellation,
missing reports and infrastructure timeouts are incomplete work, never a
successful campaign.

` ci.yml ` gives every target a smoke run: it builds the target, checks named
examples and accepted baselines, and replays the small committed corpus within the
[one-second workload budget](testing.md#heavy-runs-stay-out-of-pull-request-ci).
That keeps targets building between campaigns. Sustained searches and their
` --opt=speed ` builds stay in the campaign job.

<a id="daily-checks-of-roc-builtins"></a>

### Daily checks of Roc builtins

roc-fuzz’s builtin targets exercise compiler-provided operations such as list
sorting, strings and dictionaries. Run a bounded daily campaign against the
latest Roc nightly, for example a ten-minute total search budget distributed
across the selected targets. New nightlies can change these operations without
any change to roc-fuzz, so a daily campaign can find upstream regressions that
PR smoke runs and published examples do not expose.

Keep this as ` fuzz.yml `, with its own status and failure artifacts. It tests
the selected builtin targets and search configuration, while
[shared nightly compatibility](nightly-compatibility.md#shared-published-release-compatibility)
tests the immutable published product examples. Neither result substitutes for
the other. Investigate a failure, minimise and preserve its reproducer, and
report an upstream issue when the evidence points to Roc. Compiler-pin changes
remain a separate maintainer decision.

<a id="benchmarks"></a>

## Benchmarks

` bench.yml ` measures on a schedule and on request, never on a pull request.
` ci.yml ` gives every benchmark a smoke run, one small iteration built with
` --opt=dev ` and at most one second of workload execution, so that a benchmark
cannot fall behind the API it measures. Smoke timings are not performance
measurements and must not update baselines or fail a performance threshold.

Use the same schedule/manual event and default-branch guard as ` fuzz.yml `.
Measured runs use ` --opt=speed `, documented warmups, samples and runner
conditions. Record source and compiler identities beside the baseline and raw
measurements so a comparison is interpretable. Hosted-runner variance needs an
explicit measurement policy; do not turn timing noise into a PR failure.

For a ceiling-based allocation ratchet, fail when the count exceeds the bound
and lower the ceiling when it improves. roc-fuzz’s accepted allocation baselines
use exact matching instead, so both increases and reductions need review. See
[Allocation properties and exact baselines](testing.md#allocation-properties-and-exact-baselines)
for choosing and maintaining these checks.

<a id="linker-inputs"></a>

## Linker inputs

Platforms that consume prebuilt native inputs have a producer workflow and a
publisher workflow. These are the only route by which native build inputs are
released. [Maintain a platform](platforms.md#maintain-a-platform) explains
when a platform needs them, and
[Publish linker inputs from a pull request](linker-input-releases.md#publish-linker-inputs-from-a-pull-request)
specifies both.

