Dosu LogoDosu Logo
Ask
Join our Discord
Organization avatar
bluefinPublic
Bluefin
Documentsbluefin
composite-actions
composite-actions
Type
External
Status
Published
Created
Jun 13, 2026
Updated
Aug 6, 2026
Updated by
Dosu Bot
Source
docs/skills/composite-actions.md

Composite Actions - Authoring Skill#

Reference for writing and maintaining composite GitHub Actions in this repo.

Update this file when you discover a new pattern, workaround, or convention - in the same PR as your change.

Contents#

  • Structure
  • SHA Pinning
  • Shell steps
  • Action catalog
  • Rollout strategy
  • Adding a new action
  • Common editing pitfalls
  • CI-fix-first workflow (for agents)
  • Known workarounds
  • Self-repository composition

Sub-files (load as needed):

  • composite-actions/action-reference.md - full action-by-action reference
  • composite-actions/reusable-workflow.md - reusable-build.yml and reusable-release.yml details

Structure#

Every action lives at bootc-build/<name>/action.yml. No build system, no scripting layer - only YAML + inline shell.

Required top-level keys:

name: "bootc-build/<name>"
description: "One-line summary"
author: "projectbluefin"
inputs: { ... }
outputs: { ... } # omit if no outputs
runs:
  using: "composite"
  steps: [ ... ]

SHA Pinning#

Third-party actions (anything outside projectbluefin/) must be pinned to a full commit SHA with a version comment:

uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1

Never use floating tags (@main, @v3, @latest) on third-party actions. Renovate runs in this repo and auto-merges SHA pin and digest bumps when CI passes.

The version comment must match a released version tag (e.g. v3.10.1, not just v3). If a repo has no releases or tags, use a descriptive comment instead:

uses: ublue-os/some-action@abc123def456... # no-release, Merge PR #18 (2026-02)

First-party actions (anything inside projectbluefin/actions) use @v1 - never SHA-pin your own code:

# Correct
uses: projectbluefin/actions/bootc-build/create-release@v1
uses: projectbluefin/actions/.github/workflows/reusable-release-gate.yml@v1

# Wrong - stale SHA pins on first-party code cause silent failures
uses: projectbluefin/actions/bootc-build/create-release@abc1234... # v1

SHA pinning is a third-party supply-chain defence (prevents tag hijacking on external repos you don't control). It does not apply to code you own.

Renovate must be told to leave first-party refs alone#

config:best-practices pulls in helpers:pinGitHubActionDigests, which pins every
uses: it finds — including this repo's references to itself. Once pinned, Renovate keeps
opening "update projectbluefin/actions digest to …" PRs forever. Blocking an individual PR
only treats the symptom; the next Renovate run regenerates it, and eventually someone merges
it without noticing.

The fix is a terminal packageRules entry disabling the dep outright. It lives in both
config files:

  • .github/renovate.json5 — this repo's own Renovate config
  • default.json — the shared preset other repos consume via local>projectbluefin/actions
// Keep this LAST in packageRules: later rules win, so it must override the
// automerge rules above rather than be overridden by them.
{
  "matchManagers": ["github-actions"],
  "matchPackageNames": ["projectbluefin/actions"],
  "enabled": false
}

Why matchPackageNames: ["projectbluefin/actions"] catches everything: the
github-actions manager derives depName from the repository only, stripping the
subdirectory and the workflow filename. So projectbluefin/actions/bootc-build/chunka@v1
and projectbluefin/actions/.github/workflows/reusable-release-gate.yml@v1 both report as
depName projectbluefin/actions. One rule covers actions, composite actions, and reusable
workflows.

Scope this to projectbluefin/actions only — not projectbluefin/*. Other org repos
(projectbluefin/testsuite, projectbluefin/bonedigger) are genuinely cross-repo: this repo
does not control their tags and does not advance them, so their refs stay SHA-pinned and
Renovate must keep updating them. docs/skills/consumer-validation.md applies the same
narrow scope to the no-floating-action-tags pre-commit hook.


Shell steps#

Error handling#

Start every non-trivial shell step with:

set -euo pipefail

Use set -eux for steps where verbose trace output aids debugging (e.g., package installs).

Passing inputs to shell#

Pass inputs through the env: block - do not expand ${{ inputs.foo }} inline inside run: scripts:

# āœ… correct
env:
  IMAGE: ${{ inputs.image }}
  DIGEST: ${{ inputs.digest }}
run: |
  cosign sign -y "${IMAGE}@${DIGEST}"

# āŒ wrong - shell injection risk, harder to trace
run: |
  cosign sign -y "${{ inputs.image }}@${{ inputs.digest }}"

This applies to all expression types in run: blocks: inputs, matrix values, context values (github.actor, github.event_name), step outputs, and especially secrets. The only safe place to reference ${{ ... }} in shell is via env vars.

Glob inputs: when an input is a shell glob (e.g. shellcheck-glob), pass it via env and expand it unquoted:

env:
  SHELLCHECK_GLOB: ${{ inputs.shellcheck-glob }}
run: |
  shopt -s globstar nullglob
  # shellcheck disable=SC2086
  shellcheck ${SHELLCHECK_GLOB}

Word splitting on an env var is safe for globs but is NOT command injection - ; in a variable is not a command separator.

Secrets in run: steps#

Pass secrets via env: and reference with ${VAR}:

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
  echo "${GITHUB_TOKEN}" | podman login ...

Never do echo ${{ secrets.GITHUB_TOKEN }} directly in run:. GitHub masks it in logs but error messages and downstream uses may still leak it.

Lowercasing registry paths#

Registry/org names must be lowercased before any push or reference:

REGISTRY_LOWER="${REGISTRY,,}/${GITHUB_REPOSITORY_OWNER,,}"

Privileged operations#

Container/storage commands that need root use sudo -E to preserve the environment:

sudo -E podman push ...

github-token pattern#

Actions that need registry access or GitHub API calls take an explicit github-token input (required). The calling workflow supplies ${{ secrets.GITHUB_TOKEN }} or a PAT - the action never uses it implicitly.

inputs:
  github-token:
    description: "Token for registry login"
    required: true

Failing fast with ::error::#

Surface actionable errors using the ::error:: workflow command before exit 1:

echo "::error::signing-mode=keyless requires 'id-token: write' permission in the calling job."
exit 1

Action catalog#

Quick reference - for full details see composite-actions/action-reference.md.

ActionPurpose
setup-runnerUpdate Podman, configure BTRFS or opt-in native overlay storage, install tools
dnf-cacheRestore/save buildah layer cache
preflightValidate registry auth, normalize image refs
push-imagePush with retry, digest capture, skopeo alias tags
sign-and-publishCosign keyless/key + Syft SBOM + SLSA provenance attestation
apply-pkg-intervalsSet user.update-interval xattrs on RPM files — run before chunka
chunkaOCI-native chunkah v0.6.0 rechunking - the single rechunk implementation for all Fedora-based images
ghcr-cleanupPrune old/untagged GHCR images
detect-changesDetect changed paths, compute image-flavor build matrix
validate-prRun just check, shellcheck, hadolint, pre-commit
scan-imageTrivy CVE scan before push, SARIF upload, optional CVE issue creation
generate-tagsGenerate Bluefin/Fedora OCI alias tags (Path 2 only)
create-manifestMulti-arch OCI manifest index assembly
generate-release-notesgit-cliff Conventional Commits changelog
create-releaseFactory-standard release: SBOM diff + release card + supply chain notes
validate-pr-titleEnforce Conventional Commits PR title format

Rollout strategy#

Additive-only rule#

All changes to existing actions must be additive: new optional inputs with defaults that preserve existing behavior. Never remove or rename an input, and never change the behavior an existing caller already relies on, without a major version bump.

Valid additive change:

# Adding a new optional input with a safe default
force-compression:
  description: "Force recompression on push"
  default: "false" # ← existing callers are unaffected

Invalid (breaking) change: removing tags, renaming github-token, or changing a default that alters behavior for existing callers.

Force-compression input rationale#

The chunka and push-image actions expose an optional force-compression input (default: false). This input exists for CentOS Stream 10 and other non-Fedora consumers that need to migrate existing registry layers from gzip to zstd:chunked. Fedora consumers should leave it at the default because Fedora images are already zstd:chunked and forcing recompression strips ostree.components layer annotations.

Package cadence intervals (apply-pkg-intervals + reusable-pkg-cadence)#

bootc-build/apply-pkg-intervals is wired into reusable-build.yml between the build and rechunk steps. It sets user.update-interval xattrs on every RPM-owned file so chunkah can group packages by how often they actually update. A typical weekly update then only pulls layers containing that week's changed packages — not fonts, firmware, or other stable content that shares layers separately.

Zero-config for existing consumers: the action silently no-ops if files/pkg-intervals.tsv doesn't exist in the consumer repo. No Containerfile changes are needed.

Adopting as a new consumer:

  1. Add .github/workflows/pkg-cadence.yml to your repo:
# Trigger on your stable-push/release workflow completing — not a cron.
# This ensures you measure exactly the image users just received.
on:
  workflow_run:
    workflows: ["Execute Release"] # replace with your stable-push workflow name
    types: [completed]
  workflow_dispatch:
    inputs:
      force_reclassify:
        type: boolean
        default: false

permissions: {}

jobs:
  cadence:
    if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
    uses: projectbluefin/actions/.github/workflows/reusable-pkg-cadence.yml@v1
    with:
      image: ghcr.io/yourorg/yourimage:stable
      repo: yourorg/yourimage
      force_reclassify: ${{ inputs.force_reclassify || false }}
    secrets: inherit
  1. Run the workflow once manually (workflow_dispatch) to bootstrap files/pkg-intervals.tsv. It commits directly to main.

  2. From that point, apply-pkg-intervals runs automatically on every non-PR, non-testing build via reusable-build.yml, and the cadence workflow self-updates after each release.

  3. After ~4 weeks, bootstrap heuristics are replaced by observed churn data automatically.

Bootstrap heuristics (used for the first 4 weeks):

PatternInterval
tailscale, bootc, distrobox, fastfetch, uupdweekly
*-fonts*, google-noto-*, liberation-*, *cantarell*yearly
*firmware*, alsa-*-firmwarequarterly
everything elsemonthly

Threshold calibration (90-day window, weekly stable builds ā‰ˆ 13 builds):

Changes in windowInterval
≄ 8weekly
2–7monthly
1quarterly
0yearly

If your build cadence differs from weekly, pass force_reclassify: true after adjusting the thresholds in the reusable workflow, or open an issue to add a build_cadence input.

Breaking change policy#

If a breaking change is unavoidable:

  • Option A: create a versioned subdirectory (bootc-build/<name>/v2/action.yml) and route new callers there while old callers keep v1
  • Option B: coordinate a single wave - update all consuming repos in one PR sweep, then bump @v1

Document the blast radius (which repos, which inputs change) in the PR description. Do not merge without a link to passing CI in at least one consumer.

See consumer-validation.md for the required before-merge protocol.


Adding a new action#

  1. Create bootc-build/<name>/action.yml.
  2. Pin all external uses: to commit SHAs with version comments.
  3. Use the env: block pattern for all inputs passed to shell.
  4. If the action downloads or references any external file (Containerfile, script, config) at runtime, vendor it or verify its SHA-256 - see docs/skills/supply-chain.md.
  5. Add the action to the table in README.md.
  6. Add a row to the skill routing table in docs/SKILL.md.
  7. Add an entry to composite-actions/action-reference.md.
  8. Add the action to the catalog table in docs/skills/consumer-guide.md.

Common editing pitfalls#

Dropping with: when editing uses:#

When you change only the SHA or comment on a uses: line, it's easy to accidentally delete the with: block below it. The result is a valid-looking YAML step where uses: runs but all inputs are silently dropped - actionlint catches this on push.

# āŒ broken - with: block dropped, all inputs silently gone
- name: Upload artifact
  uses: actions/upload-artifact@abc123 # v7.0.1
    path: /tmp/output/ # ← this is now orphaned YAML, not under with:

# āœ… correct
- name: Upload artifact
  uses: actions/upload-artifact@abc123 # v7.0.1
  with:
    path: /tmp/output/

Always verify the with: block is still present after editing a uses: line. Actionlint enforces this but only on push - not in local editors.

Multi-line strings in run: blocks#

A double-quoted multi-line string inside a YAML run: block breaks shellcheck (and therefore
actionlint). The newlines inside the YAML block scalar cause the parser to see subsequent lines as
stray YAML keys rather than shell string content.

# āŒ broken - shellcheck sees an unclosed double-quoted string
- run: |
    git commit -m "subject line

Assisted-by: foo
Co-authored-by: bar" # actionlint: SC1072 / SC1073 / unexpected YAML key

# āœ… correct - use ANSI-C quoting ($'...') to embed newlines
- run: |
    msg="subject line"$'\n\n'"Assisted-by: foo"$'\n'"Co-authored-by: bar"
    git commit -m "${msg}"

Use $'...\n...' concatenation whenever a shell string must contain literal newlines inside a
YAML block scalar. Heredocs are also acceptable for longer messages.


CI-fix-first workflow (for agents)#

When an agent working in a consuming repo (bluefin, aurora, bazzite...) discovers a CI issue that involves duplicated inline steps or pinned third-party actions, the fix belongs here first:

  1. Check if an action already exists — scan the catalog above. If the shared action doesn’t exist yet, create it here (follow ā€œAdding a new actionā€ above).
  2. Open a PR in this repo on a feature branch with the new or updated action.
  3. Open a draft PR in the consumer repo using @v1 references (no SHA pinning needed). CI must pass there before this repo’s PR merges.
  4. Merge this repo’s PR, then advance @v1 to the new main HEAD (see @v1 runbook in AGENTS.md). Consumer repos pick up the change automatically on their next workflow run.

What belongs here vs. in the consumer repo:

Belongs here (projectbluefin/actions)Stays in the consumer repo
Shared step sequences (lint, validate, detect-changes)Caller-specific permissions scoping
Third-party action pins (hadolint, install-action, paths-filter)secrets: inherit decisions
Reusable logic used in ≄2 workflows or reposRepo-specific Justfile recipes
Path-filter definitions shared across workflowsWorkflow scheduling and triggers

Never add a new inline uses: for a third-party action in a consumer workflow if that action is already wrapped here. Inline pins create Renovate drift across all consumer workflows - centralize them.


Known workarounds#

WorkaroundLocationIssue
chown /run/user/$UID/containers before loginpush-image, create-manifestEarlier sudo podman login can leave root-owned auth files that break later user-space login
native-overlay: true with runner Podmansetup-runnerA rootful fuse-overlayfs image mount beneath a kernel overlay can return ESTALE when scanned inside another container; reset storage and verify kernel overlay with no mount_program before building

Trigger patterns#

Never use a cron to run a workflow that depends on another workflow completing. Crons embed a timing assumption that drifts and creates silent races. Use workflow_run with a conclusion == 'success' guard instead:

on:
  workflow_run:
    workflows: ["Execute Release"]
    types: [completed]
jobs:
  my-job:
    if: github.event.workflow_run.conclusion == 'success'

Always add workflow_dispatch alongside workflow_run so the workflow can be triggered manually without waiting for the upstream workflow to run.
| chmod 777 before cache save | dnf-cache | actions/cache#1533 - root-owned files break cache agent |
| chown ~/.sigstore before cosign | sign-and-publish | Runner sigstore cache created with wrong ownership |
| podman upgraded from Ubuntu resolute | setup-runner | Ubuntu 24.04 podman too old for ostree.components annotations + zstd:chunked push |
| -v $(pwd):/run/src + --security-opt=label=disable | chunka | buildah < v1.44 drops bind-mounts without these; needed for the OCI output dir (out/) to survive to the final stage |
| sudo rm -rf out | chunka | Containerfile.splitter leaves out/ dir in CWD (v0.6.0+; was out.ociarchive in v0.5.0); stale dir breaks re-runs |
| sudo podman save \| podman load | chunka | buildah (root) and podman (user) use separate container stores |
| sudo buildah push --authfile ${RUNNER_TEMP}/push-auth.json | push-image | sudo podman push cannot reach root container storage; --authfile bypasses XDG_RUNTIME_DIR inaccessible to sudo on Ubuntu 24.04 |
| Largest-free tmpdir selection + sudo mktemp -d -p <dir> + sudo chmod 755 for overlay dirs | chunka | BTRFS volume is root-owned; default /var/tmp is only ~1 GB on runners with BTRFS loopback; picks /var/lib/containers (~49 GB) when it has more free space; sudo mktemp needed because the dir is root-owned |

Reusable workflow caller permissions ceiling#

GitHub enforces the caller's permissions: block as a hard ceiling for every callee job. A callee
job declaring contents: write is silently downgraded to contents: read if the caller only
grants contents: read.

Pattern: audit the caller's top-level permissions: block against every job the callee
declares, and ensure the caller grants the union of all permissions any callee job requires.

# lifecycle-caller.yml: ceiling must include write because on-pr-lgtm inside
# lifecycle.yml calls `gh pr merge --auto`, which needs contents: write
permissions:
  issues: write
  pull-requests: write
  contents: write # required even though most jobs only need read

Silent failure mode: the callee job runs without errors, but any operation that needs write
(merge, push, tag) returns a silent 403.

PR branch rebase with conflicting intermediate commits#

When a PR branch contains an intermediate "stepping stone" commit that conflicts with main,
do not attempt a full rebase of the branch. Instead:

  1. Create a new branch off origin/main
  2. git cherry-pick <final-commit-SHA> - skip the intermediate commit entirely
  3. Resolve any conflict in the final commit (usually trivial)
  4. Force-push to the PR branch

This avoids pulling obsolete intermediate state into main and produces a clean single commit.

Self-repository composition#

When implementation code in this repository invokes another action or reusable workflow owned by this repository, use the self-repository prefix $/ rather than a qualified projectbluefin/actions/...@ref or workspace-relative ./ reference:

steps:
  - uses: $/bootc-build/push-image

jobs:
  gate:
    uses: $/.github/workflows/reusable-release-gate.yml

$/ resolves the action or workflow from the repository and commit currently executing. This is distinct from ./, which resolves against the caller's checked-out workspace and therefore is unsafe for reusable workflows invoked by another repository. Keep actions/checkout when the caller's source tree, Justfile, artifacts, or git operations are needed; $/ does not replace a checkout used for caller-owned files.

Do not convert third-party or cross-repository references, including consumer-facing projectbluefin/actions/...@v1 examples and references in comments that document how consumers call this repository. The syntax requires GitHub Actions runner 2.336.0 or newer. The repository's pinned actionlint 1.7.7 predates this syntax, so .github/actionlint.yaml scopes parser ignores to the two affected messages until actionlint supports it. Validate with:

python3 scripts/check-self-repository-references.py
python3 -m pytest tests/test_self_repository_references.py -q
actionlint

Gotcha: escaping $ in .github/actionlint.yaml ignore patterns#

paths.<glob>.ignore entries are Go regular expressions matched against the
error message text. In a YAML single-quoted scalar a backslash is already
literal — '' is the only escape — so writing '\\$/.+' compiles to the regex
\\$/.+ (literal backslash, then an end-of-line anchor) and silently matches
nothing. Use a single backslash:

- 'specifying action "\$/.+" in invalid format because ref is missing' # correct
- 'specifying action "\\$/.+" in invalid format because ref is missing' # never matches

A dead ignore pattern fails open, not closed: actionlint keeps reporting and CI
stays red, so verify any new pattern locally with the same actionlint version
pinned in .pre-commit-config.yaml before pushing. Always confirm the ignore is
scoped rather than wholesale by checking that an unrelated injected error is
still reported.

Do not pass a bare -ignore flag to the actionlint pre-commit hook. -ignore
takes a pattern argument and will otherwise consume the next value on the
command line. Repository-wide ignores belong in .github/actionlint.yaml.

Documents
Changelogs
Bluefin LTS Errata
Pin Log
2024-12-30-ublue-2024-wrapup
2025-01-01-announcements-archive
2025-02-23-february-update
2025-05-14-bluefin-f42
2025-06-23-bazaar-default
2025-07-06-fresh-bluefin-isos-and-bazaar
2025-07-11-four-years-of-ublue
2025-07-15-bold-brew
2025-07-17-ask-projectbluefin
2025-08-01-august-wallpapers
2025-08-08-bluefin-on-lfx
2025-08-29-github-oss-fund
2025-09-17-color-with-bluefin
2025-09-18-bluefin-lts-and-gdx
2025-09-23-LTS-at-indiafoss
2025-10-28-bluefin-autumn
2025-10-31-fastfetch-metrics
2025-11-15-cloud-native-milestone-15-million
2025-11-16-coreos-desktop
2025-11-24-new-just-and-bbrew
2025-11-27-bluefin-and-paleoartists
2025-11-30-report
2025-12-04-flatpak-support-in-brewfiles
2025-12-06-interview-with-michael-tunnell
2025-12-07-documentation-updates
2025-12-14-huntress-holiday-wallpapers
2025-12-15-easier-homebrew-installation-for-custom-images
2025-12-27-upcoming-homebrew-cli-changes
2025-12-30-bluefin-2025-wrapup
2025-12-31-report
2026-01-03-modernizing-custom-images
2026-01-31-report
2026-02-01-automated-reports-changelogs
2026-02-28-report
2026-03-13-bluefin-cli-mac-wsl
2026-03-27-bluefin-lts-gnome-49-50
2026-03-29-composefs-fisherman
2026-03-29-composefs-fisherman-design
2026-03-30-boot-verification
2026-03-30-boot-verification-design
2026-03-30-proportional-progress-tracking
2026-03-30-proportional-progress-tracking-design
2026-03-31-report
2026-04-02-state-of-ecosystem
2026-04-19-dakota-alpha-1
2026-04-30-report
2026-05-05-bluefin-spring
2026-05-12-bluefin-spring-2026
2026-05-13-bluefin-spring-2026-2
2026-05-14-making-our-own-fate
2026-05-15-hummingbird
2026-05-23-gradia-capture-bluefin
2026-05-28-knuckle
2026-05-31-report
2026-06-11-promotion-pr-body
AGENTS
AGENTS
AGENTS
AGENTS
AGENTS
AGENTS
Building a Custom Bluefin Image
CI_CD_GUIDE
CLAUDE
CLAUDE
CONTRIBUTING
CONTRIBUTING
CONTRIBUTING
CONTRIBUTING
CONTRIBUTING
FAQ
How can I install Double Commander on Bluefin (an immutable Linux OS), coming from Arch Linux?
How can you fix random GPU artifacts and hard freezes on an AMD Framework 16 laptop running Linux (Bluefin/Fedora)?
How do I install the Cloudflare WARP (One) client on Bluefin?
How to Reset a User's Home Directory to Defaults on Bluefin
INDEX
MAINTAINERS
MULTI_VARIANT_BUILD
PITFALLS
PULL_REQUEST_TEMPLATE
Plex Media Server
README
README
README
README
README
README
README
README
README
SECURITY
SECURITY
SECURITY
SKILL
SKILL
SKILL
SKILL
TRANSLATING
Update Process
Which Bluefin ISO is recommended for a Lenovo Yoga Pro 9i with an NVIDIA RTX 4050 for Python data science and Machine Learning with CUDA?
about-monthly-reports
action-reference
actionadon
add-package
administration
agentic-contributing
ai
analytics
architect
architecture
artwork
blog-poster.agent
bluefin-dx
bluefin-gdx
bonedigger-agent-donation
bonedigger-lifecycle
bonedigger-overview
bonedigger-templates
bonedigger-ujust
bst-overrides
build
build
buildstream
ci
ci
ci
code-of-conduct
command-line
composite-actions
consumer-guide
consumer-validation
contributing
contributors
conventional-commit.prompt
copilot-instructions
copilot-instructions
copilot-instructions
copilot-instructions
dakota-tpm2-validation
debugging
demo-mode-e2e
determinism
dinosaurs
downloads
downloads-testing
driver-versions
e2e-ci
e2e-feature-verification
encryption-matrix
factory-operations
failure-paths
feedback-loop
gstreamer-codec-validation
human-gates
images
index
index
installation
installer
introduction
knuckle
label-workflow
libpastry-integration
live-iso
local-ota
lore
lts
luks-testing
merge-queue
mission
multi-arch
music
not-bluefin
oci-assembly
oci-layers
onboarding
overview
packaging-binaries
packaging-gnome-extensions
packaging-go
packaging-rust
packaging-zig
patch-junctions
patches
pr-checklist
pr-review
press-kit
projects
pull_request_template
pull_request_template
pull_request_template
qr-phone-companion
quickstart
r2-promotion
release-qualification
remove-package
reports
reusable-workflow
skill-improvement
supply-chain
t2-mac
testing
tips
troubleshooting
ujust-recipes
update-refs
upgrade-and-migration
ux-ubuntu-provision
values
variants
vm-stack
workflow