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:
- Pre-build — validates the Flatpak manifest before compilation starts.
- Post-build — validates the generated OSTree repository after
flatpak-buildercompletes.
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:
| Level | Mechanism |
|---|---|
| CLI flags | --linter-exception / --linter-exceptions-file |
| Environment variables | AETHERPAK_LINTER_EXCEPTIONS / AETHERPAK_LINTER_EXCEPTIONS_FILE |
| Global config | linter.exceptions / linter.ignore_rules / linter.exceptions_file in aetherpak.yaml |
| Per-app config | linter.exceptions / linter.exceptions_file per app in aetherpak.yaml |
| JSON exceptions file | Keyed 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_linterand the per-apprun-linterfield 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 :
| Flag | Description |
|---|---|
--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 :
| Field | Type | Description |
|---|---|---|
strict | boolean | If true, any lint failure causes the build to fail. If false, failures are logged as warnings. Defaults to true. |
ignore_rules | list[string] | List of flatpak-builder-lint rule IDs to suppress. |
exceptions | list[string] | Alternative/alias for ignore_rules. Both are merged and deduplicated before use. |
exceptions_file | string | Relative path to a JSON exceptions file (see Exception File Format). |
Note:
ignore_rulesandexceptionsare 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 :
| Field | Type | Description |
|---|---|---|
strict | boolean | Override strict mode for this app. |
ignore_rules | list[string] | Additional rules to suppress (merged with global ignore_rules). |
exceptions | list[string] | Additional exceptions (merged with global exceptions). |
exceptions_file | string | Override 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#
| Key | Description |
|---|---|
"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):
aetherpak.yaml— globallinter.exceptions_fileor per-applinter.exceptions_file- Environment variable —
AETHERPAK_LINTER_EXCEPTIONS_FILE(orAETHERPAK_LINTER_EXCEPTIONSif the value ends in.json) - 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_FILEorAETHERPAK_LINTER_EXCEPTIONS(if.json-suffixed) override the exceptions file pathAETHERPAK_LINTER_EXCEPTIONS(comma-separated) overrides the inline exceptions list- Explicit
--linter-exceptions-fileor--linter-exceptionCLI 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
linterblock, it inherits the entire globallinterconfig (strict setting,ignore_rules,exceptions, andexceptions_file) verbatim. - If an app has a
linterblock, the globalignore_rulesandexceptionsare merged into the app's lists (deduplicated). The app's own entries take priority in ordering, and the globalexceptions_fileis 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#
| Stage | Source | Precedence |
|---|---|---|
| 1 | AetherPak built-in defaults | Lowest |
| 2 | linter.ignore_rules in aetherpak.yaml | ↑ |
| 3 | linter.exceptions in aetherpak.yaml (global + per-app merged) | ↑ |
| 4 | Exceptions file (linter.exceptions_file) — app-specific + wildcard | ↑ |
| 5 | AETHERPAK_LINTER_EXCEPTIONS_FILE / AETHERPAK_LINTER_EXCEPTIONS env vars | ↑ |
| 6 | --linter-exception / --linter-exceptions-file CLI flags | Highest |
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 .
| Variable | Description |
|---|---|
AETHERPAK_LINTER_EXCEPTIONS_FILE | Path to a JSON exceptions file. Takes precedence over AETHERPAK_LINTER_EXCEPTIONS when both are set. |
AETHERPAK_LINTER_EXCEPTIONS | Comma-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_FILEwhen managing a shared exceptions file across multiple repositories or CI jobs. UseAETHERPAK_LINTER_EXCEPTIONSfor 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#
-
Merge all sources — AetherPak combines built-in defaults,
ignore_rules,exceptions, the exceptions file, and CLI flags into a single deduplicated rule list . -
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 howflatpak-builder-lintresolves the app identity . -
Invoke
flatpak-builder-lintwith 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 -
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 -
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 :
| Mode | Behavior |
|---|---|
strict: true (default) | A lint failure causes the build to return an error and halt. |
strict: false | A 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: falsein youraetherpak.yamltemporarily 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 :
| Input | Type | Default | Description |
|---|---|---|---|
run-linter | boolean | true | Whether 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.
Recommended Approach#
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-levelenvis effective.