Documents
Working with Linter Exceptions
Working with Linter Exceptions
Type
Document
Status
Published
Created
Jun 4, 2026
Updated
Jun 4, 2026

Working with Linter Exceptions#

1. Overview#

AetherPak integrates the upstream flatpak-builder-lint tool maintained by the Flathub infrastructure team. Rather than implementing its own validation logic, AetherPak installs the tool directly from the canonical upstream source — ensuring that lint results are consistent with what Flathub itself would produce . When linting is enabled, it runs twice during a build:

  1. Pre-build — validates the Flatpak manifest before compilation starts.
  2. Post-build — validates the generated OSTree repository after flatpak-builder completes.

For cases where linting is not desired at all, both the CLI and GitHub Actions expose a simple boolean toggle to disable it entirely. For finer-grained control, AetherPak supports an exception-based configuration system that lets you suppress specific lint rules at multiple levels:

LevelMechanism
CLI flags--linter-exception / --linter-exceptions-file
Environment variablesAETHERPAK_LINTER_EXCEPTIONS / AETHERPAK_LINTER_EXCEPTIONS_FILE
Global configlinter.exceptions / linter.ignore_rules / linter.exceptions_file in aetherpak.yaml
Per-app configlinter.exceptions / linter.exceptions_file per app in aetherpak.yaml
JSON exceptions fileKeyed by app ID or "*" wildcard

All of these sources are merged additively before each lint invocation — per-app exceptions extend global exceptions rather than replacing them, and AetherPak's own built-in defaults are always applied first .

2. Disabling the Linter#

The simplest way to skip all linting is to disable it entirely. AetherPak provides controls for this at the CLI, GitHub Actions, and config-file levels.

CLI#

Pass the --run-linter flag to explicitly enable linting on aetherpak build, aetherpak publish, or aetherpak release. Linting is off by default unless this flag is present :

# Linting disabled (default — flag not provided)
aetherpak build --manifest apps/org.example.App/manifest.json --arch x86_64

# Linting enabled
aetherpak build --manifest apps/org.example.App/manifest.json --arch x86_64 --run-linter

GitHub Actions#

The reusable publish workflow and the composite build action each expose a run-linter input. Set it to false to skip linting for a given run :

# Reusable workflow — manifest mode
jobs:
  publish:
    uses: aetherpak/actions/.github/workflows/publish.yml@v3
    with:
      manifest-path: apps/org.example.App/manifest.json
      run-linter: false

When run-linter is true (the default), the workflow passes --run-linter to the aetherpak build invocation .

aetherpak.yaml#

You can also control linting in your configuration file. The defaults.run_linter field acts as the global default for all apps, and each app can override it with its own run-linter field :

# aetherpak.yaml

# Disable linting globally for all apps
defaults:
  run_linter: false

apps:
  - id: org.example.AppOne
    manifest: apps/org.example.AppOne/manifest.json
    # Inherits run_linter: false from defaults

  - id: org.example.AppTwo
    manifest: apps/org.example.AppTwo/manifest.json
    run-linter: true # Override: enable linting for this app only

Note: defaults.run_linter and the per-app run-linter field control whether linting runs at all. To suppress specific rules rather than skipping the linter entirely, use exception configuration as described in the sections below.

3. CLI Configuration Flags#

When you need to pass exceptions at runtime without modifying aetherpak.yaml, the CLI provides two flags available on aetherpak build, aetherpak publish, and aetherpak release :

FlagDescription
--linter-exceptions-file <path>Path to a JSON exceptions file (see Exception File Format)
--linter-exception <value>A single exception rule name to suppress; repeatable

Examples#

# Suppress a single rule
aetherpak build \
  --manifest apps/org.example.App/manifest.json \
  --run-linter \
  --linter-exception appstream-screenshot-missing

# Suppress multiple rules inline
aetherpak build \
  --manifest apps/org.example.App/manifest.json \
  --run-linter \
  --linter-exception appstream-screenshot-missing \
  --linter-exception appstream-summary-too-long

# Point to a JSON exceptions file
aetherpak build \
  --manifest apps/org.example.App/manifest.json \
  --run-linter \
  --linter-exceptions-file .linter-exceptions.json

Precedence#

CLI flags have the highest precedence in the merging hierarchy. When --linter-exception or --linter-exceptions-file is explicitly provided on the command line, it overrides any value set via environment variable or configuration file . This makes CLI flags the right choice for one-off overrides during local development or debugging, while aetherpak.yaml remains the canonical source for committed, reproducible CI configuration.

4. aetherpak.yaml Configuration#

aetherpak.yaml is the primary place to configure linting behavior that should be consistent across all CI runs. Both a global linter block and per-app linter overrides are supported .

Global Linter Configuration#

The top-level linter block applies to all apps in the repository unless overridden at the app level :

# aetherpak.yaml

linter:
  strict: false # If true, lint failures are fatal (default: true)
  ignore_rules: # Rules to suppress (by rule ID)
    - no-appstream-screenshot-urls
  exceptions: # Alias/alternative to ignore_rules
    - exception-1
    - exception-2
  exceptions_file: path/to/exceptions.json # Path to a JSON exceptions file

Global linter fields :

FieldTypeDescription
strictbooleanIf true, any lint failure causes the build to fail. If false, failures are logged as warnings. Defaults to true.
ignore_ruleslist[string]List of flatpak-builder-lint rule IDs to suppress.
exceptionslist[string]Alternative/alias for ignore_rules. Both are merged and deduplicated before use.
exceptions_filestringRelative path to a JSON exceptions file (see Exception File Format).

Note: ignore_rules and exceptions are functionally equivalent and both map to the same merged exception list at runtime. You may use either or both; duplicates are automatically removed .

Per-App Linter Configuration#

Individual apps can extend or refine the global linter configuration using their own linter block. Per-app exceptions are additive — they extend the global exceptions rather than replacing them :

# aetherpak.yaml

linter:
  exceptions:
    - global-exception-for-all-apps

apps:
  - id: org.example.AppOne
    manifest: apps/org.example.AppOne/manifest.json
    linter:
      exceptions:
        - app-specific-exception # Added on top of global exceptions
      exceptions_file: path/to/app-specific-exceptions.json # Overrides global exceptions_file

  - id: org.example.AppTwo
    manifest: apps/org.example.AppTwo/manifest.json
    # No linter block — inherits global linter config entirely

Per-app linter fields :

FieldTypeDescription
strictbooleanOverride strict mode for this app.
ignore_ruleslist[string]Additional rules to suppress (merged with global ignore_rules).
exceptionslist[string]Additional exceptions (merged with global exceptions).
exceptions_filestringOverride the exceptions file for this app. If omitted, inherits the global exceptions_file.

Tip: If a per-app config does not specify an exceptions_file, it automatically inherits the global one . This means a single exceptions file can cover all apps while still allowing individual apps to add further inline exceptions.

Enabling the Linter via defaults#

To enable linting for all apps by default without needing per-app flags:

# aetherpak.yaml

defaults:
  run_linter: true # Run linter for every app unless individually overridden

linter:
  strict: false
  exceptions:
    - appstream-screenshot-missing

5. Exception File Format#

AetherPak uses the same JSON exceptions file format as the upstream flatpak-builder-lint tool, making it easy to reuse exception files from Flathub workflows .

The file is a JSON object mapping app IDs to arrays of rule names to suppress:

{
  "org.example.App": [
    "appstream-screenshot-missing",
    "appstream-summary-too-long"
  ],
  "com.example.Other": [
    "appstream-license-missing"
  ],
  "*": [
    "global-exception-for-all-apps"
  ]
}

Keys#

KeyDescription
"org.example.App"Exceptions applied only when linting the app with this exact ID.
"*"Exceptions applied to every app, regardless of ID (wildcard).

When AetherPak processes an exceptions file, it extracts rules for the specific app ID being built and for the "*" wildcard key, then merges both into the unified exception list . This means you can use a single exceptions file to manage both global and per-app rules in one place.

Pointing to an Exceptions File#

The exceptions file path can be set in three ways (in ascending precedence):

  1. aetherpak.yaml — global linter.exceptions_file or per-app linter.exceptions_file
  2. Environment variableAETHERPAK_LINTER_EXCEPTIONS_FILE (or AETHERPAK_LINTER_EXCEPTIONS if the value ends in .json)
  3. CLI flag--linter-exceptions-file <path>

Note: Rule names in the exceptions file must exactly match the rule IDs reported by flatpak-builder-lint. See the upstream linter documentation for the full list of available rules.

6. How Exceptions Are Merged (Additive)#

AetherPak collects exceptions from all configured sources and merges them into a single unified list before invoking flatpak-builder-lint. The merge is strictly additive with deduplication — each stage appends new rules without removing anything contributed by an earlier stage .

The merge proceeds in this order, from lowest to highest precedence:

Step 1 — AetherPak Built-in Defaults#

AetherPak always starts with two built-in default exceptions :

appstream-external-screenshot-url
appstream-screenshots-not-mirrored-in-ostree

These are applied automatically because self-hosted AetherPak repositories do not mirror screenshots into the OSTree commit like Flathub does. Without these defaults, every app hosted outside of Flathub would fail these checks even when everything else is correct.

Step 2 — Global linter.ignore_rules#

Rules from the global linter.ignore_rules list in aetherpak.yaml are appended, skipping any already present .

Step 3 — Global/App linter.exceptions#

The resolved linter.exceptions list (global or per-app, already merged during config normalization) is appended, again with deduplication .

Step 4 — Exceptions File#

If an exceptions_file path is set (from global config, per-app override, or environment variable), the file is loaded and two sets of rules are extracted :

  • Rules keyed to the specific app ID being linted
  • Rules keyed to the "*" wildcard

Both are merged into the accumulated rule list with deduplication.

Inheritance: If a per-app config does not specify its own exceptions_file, it inherits the global one. This means a shared file can cover all apps while per-app inline exceptions layer on top .

Step 5 — Environment Variables and CLI Flags#

Finally, the resolveLinterExceptions() function in the command layer applies environment variable and CLI flag overrides :

  • AETHERPAK_LINTER_EXCEPTIONS_FILE or AETHERPAK_LINTER_EXCEPTIONS (if .json-suffixed) override the exceptions file path
  • AETHERPAK_LINTER_EXCEPTIONS (comma-separated) overrides the inline exceptions list
  • Explicit --linter-exceptions-file or --linter-exception CLI flags take precedence over everything else

Config Normalization#

The additive merging of per-app and global config happens at config load time in Config.Normalize() :

  • If an app has no linter block, it inherits the entire global linter config (strict setting, ignore_rules, exceptions, and exceptions_file) verbatim.
  • If an app has a linter block, the global ignore_rules and exceptions are merged into the app's lists (deduplicated). The app's own entries take priority in ordering, and the global exceptions_file is used as a fallback if the app does not specify one.

This means per-app exceptions always extend global exceptions — there is no way for a per-app config to accidentally discard exceptions set at the global level.

Summary Table#

StageSourcePrecedence
1AetherPak built-in defaultsLowest
2linter.ignore_rules in aetherpak.yaml
3linter.exceptions in aetherpak.yaml (global + per-app merged)
4Exceptions file (linter.exceptions_file) — app-specific + wildcard
5AETHERPAK_LINTER_EXCEPTIONS_FILE / AETHERPAK_LINTER_EXCEPTIONS env vars
6--linter-exception / --linter-exceptions-file CLI flagsHighest

7. Environment Variables#

Environment variables provide a way to inject exception configuration at CI runtime without modifying aetherpak.yaml or the CLI invocation. They are processed by the resolveLinterExceptions() function before CLI flags are applied .

VariableDescription
AETHERPAK_LINTER_EXCEPTIONS_FILEPath to a JSON exceptions file. Takes precedence over AETHERPAK_LINTER_EXCEPTIONS when both are set.
AETHERPAK_LINTER_EXCEPTIONSComma-separated list of exception rule names or, if the value ends in .json, treated as a file path.

Behavior Details#

AETHERPAK_LINTER_EXCEPTIONS_FILE sets the exceptions file path and overrides any exceptions_file value from aetherpak.yaml :

export AETHERPAK_LINTER_EXCEPTIONS_FILE=".ci/linter-exceptions.json"
aetherpak build --manifest manifest.json --run-linter

AETHERPAK_LINTER_EXCEPTIONS accepts either a comma-separated rule list or a .json file path :

# Inline comma-separated rules
export AETHERPAK_LINTER_EXCEPTIONS="appstream-screenshot-missing,appstream-summary-too-long"
aetherpak build --manifest manifest.json --run-linter

# Shorthand file path (detected by .json suffix)
export AETHERPAK_LINTER_EXCEPTIONS=".ci/linter-exceptions.json"
aetherpak build --manifest manifest.json --run-linter

Precedence#

Environment variables sit above aetherpak.yaml configuration but below explicit CLI flags in the resolution order . This makes them suitable for injecting exceptions in CI workflows (for example, via GitHub Actions secrets or repository variables) without requiring changes to committed configuration files.

Tip: Use AETHERPAK_LINTER_EXCEPTIONS_FILE when managing a shared exceptions file across multiple repositories or CI jobs. Use AETHERPAK_LINTER_EXCEPTIONS for lightweight, one-off rule suppression in environments where modifying files is impractical.

8. How Exceptions Are Passed to flatpak-builder-lint#

Once all exception sources have been resolved and merged, AetherPak writes them to a temporary file and passes that file to flatpak-builder-lint using the tool's native user-exceptions interface.

Runtime Steps#

  1. Merge all sources — AetherPak combines built-in defaults, ignore_rules, exceptions, the exceptions file, and CLI flags into a single deduplicated rule list .

  2. Create a temporary exceptions file — If any rules are present after merging, AetherPak creates a temporary JSON file at os.CreateTemp(..., "aetherpak-linter-*.json") :

    {
      "org.example.App": ["rule-one", "rule-two"],
      "*": ["rule-one", "rule-two"]
    }
    

    AetherPak writes the merged rule list under both the specific app ID and the "*" wildcard key to ensure the rules are picked up regardless of how flatpak-builder-lint resolves the app identity .

  3. Invoke flatpak-builder-lint with the exceptions file — The temporary file path is appended to the lint command as --exceptions --user-exceptions <temp-file> :

    flatpak-builder-lint manifest path/to/manifest.json \
      --exceptions --user-exceptions /tmp/aetherpak-linter-12345.json
    
  4. Post-build repo linting — The same merged exception list and temporary file are reused for repository linting after the build completes :

    flatpak-builder-lint repo path/to/repo \
      --exceptions --user-exceptions /tmp/aetherpak-linter-12345.json
    
  5. Cleanup — The temporary file is automatically removed via defer os.Remove(...) when the build function returns .

Strict vs. Non-Strict Mode#

The linter.strict setting controls how lint failures are handled :

ModeBehavior
strict: true (default)A lint failure causes the build to return an error and halt.
strict: falseA lint failure is logged as a warning; the build continues.

Note: Strict mode applies to both manifest and repository linting. If you want to experiment with exception configuration without risking CI failures, set linter.strict: false in your aetherpak.yaml temporarily while tuning your exception rules.

9. GitHub Actions Integration#

The AetherPak GitHub Actions do not expose dedicated inputs for exception configuration. Exception rules are managed entirely through aetherpak.yaml committed to your repository — the CLI reads the config at build time and handles all exception logic internally.

What the Actions Provide#

The reusable publish workflow and the composite build action each expose a single linter-related input :

InputTypeDefaultDescription
run-linterbooleantrueWhether to run flatpak-builder-lint during the build.

When run-linter is true, the action passes --run-linter to the aetherpak build CLI command . All exception configuration is delegated to the CLI and its config-file resolution chain.

Commit your exception configuration to aetherpak.yaml for reproducible CI behavior:

# aetherpak.yaml

defaults:
  run_linter: true

linter:
  strict: false
  exceptions_file: .ci/linter-exceptions.json

apps:
  - id: org.example.AppOne
    manifest: apps/org.example.AppOne/manifest.json

  - id: org.example.AppTwo
    manifest: apps/org.example.AppTwo/manifest.json
    linter:
      exceptions:
        - appstream-screenshot-missing # Only applies to AppTwo

With this setup, the GitHub Actions workflow requires no exception-specific configuration at all:

# .github/workflows/publish.yml
jobs:
  publish:
    uses: aetherpak/actions/.github/workflows/publish.yml@v3
    with:
      config: aetherpak.yaml
      # run-linter defaults to true; exceptions come from aetherpak.yaml

Injecting Exceptions at Runtime#

If you need to inject exceptions without modifying aetherpak.yaml — for example, to suppress a flaky rule during a specific release — use the AETHERPAK_LINTER_EXCEPTIONS environment variable via GitHub Actions repository variables or step-level env:

- name: Build and publish
  uses: aetherpak/actions/.github/workflows/publish.yml@v3
  with:
    config: aetherpak.yaml
  env:
    AETHERPAK_LINTER_EXCEPTIONS: "appstream-screenshot-missing,appstream-summary-too-long"

Note: Environment variables set on the job or step that calls the reusable workflow are not automatically forwarded into the reusable workflow's jobs. For workflows using config mode, prefer committing exceptions to aetherpak.yaml. For manifest mode with the composite build action, step-level env is effective.

Working with Linter Exceptions | Dosu