Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/_rpm-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,54 @@ jobs:
echo "SPEC_BASENAME=$(basename "$SPEC_FILE")" >> $GITHUB_ENV
echo "EXTRACT_DIR=${EXTRACT_DIR}" >> $GITHUB_ENV

- name: Verify tokenless component contract
if: inputs.component == 'tokenless'
run: |
# Component contract guard for the nightly RPM path: this workflow
# processes the archived spec and calls rpmbuild directly, so it
# never runs the build_tokenless guard in scripts/rpm-build.sh.
# Mirror that assertion here: derive the component name from
# .anolisa/component.toml.in, verify it matches the RPM package
# name, and require an exact "Provides: anolisa-component(<name>)"
# line in the processed spec, so spec drift fails the build
# instead of publishing an RPM that silently lacks the capability
# used by `anolisa install tokenless` (GH-2836).
COMPONENT_IN=$(find "$EXTRACT_DIR" -path '*/.anolisa/component.toml.in' | head -1)
if [ -z "$COMPONENT_IN" ]; then
echo "ERROR: component contract template (.anolisa/component.toml.in) not found in source archive"
exit 1
fi

COMPONENT_NAME=$(awk '
$0 == "[component]" { in_component = 1; next }
in_component && /^\[/ { exit }
in_component && /^name = / {
value = $0
sub(/^name = "/, "", value)
sub(/"$/, "", value)
print value
exit
}
' "$COMPONENT_IN")
if [ -z "$COMPONENT_NAME" ]; then
echo "ERROR: could not parse component name from $COMPONENT_IN"
exit 1
fi

SPEC_NAME=$(grep -E '^Name:' "$HOME/rpmbuild/SPECS/$SPEC_BASENAME" | awk '{print $2}' | tr -d ' \t')
if [ "$COMPONENT_NAME" != "$SPEC_NAME" ]; then
echo "ERROR: tokenless identity mismatch: RPM name '$SPEC_NAME', component name '$COMPONENT_NAME'"
exit 1
fi

if ! grep -Fqx "Provides: anolisa-component($COMPONENT_NAME)" "$HOME/rpmbuild/SPECS/$SPEC_BASENAME"; then
echo "ERROR: tokenless spec must provide anolisa-component($COMPONENT_NAME)"
exit 1
fi

echo "TOKENLESS_COMPONENT_NAME=$COMPONENT_NAME" >> "$GITHUB_ENV"
echo "tokenless component contract OK: $SPEC_NAME provides anolisa-component($COMPONENT_NAME)"

- name: Build RPM
run: |
rpmbuild -bb --nodeps \
Expand All @@ -274,6 +322,21 @@ jobs:
done < <(find /tmp/rpm-output/ -maxdepth 1 -type f -name "*.rpm" -print)
fi

if [ "${{ inputs.component }}" = "tokenless" ]; then
# Verify the built artifact actually publishes the component
# capability; this catches cases where rpmbuild silently drops a
# malformed Provides line that passed the spec text guard.
while IFS= read -r rpm_path; do
echo "Verifying tokenless RPM capabilities: $rpm_path"
if ! rpm -qp --provides "$rpm_path" | grep -Eq "^anolisa-component\(${TOKENLESS_COMPONENT_NAME}\)([[:space:]]|$)"; then
echo "ERROR: $rpm_path does not provide anolisa-component(${TOKENLESS_COMPONENT_NAME})"
echo "Actual provides:"
rpm -qp --provides "$rpm_path"
exit 1
fi
done < <(find /tmp/rpm-output/ -maxdepth 1 -type f -name "*.rpm" -print)
fi

- name: Upload RPM artifact
uses: actions/upload-artifact@v4
with:
Expand Down
32 changes: 32 additions & 0 deletions scripts/rpm-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,38 @@ build_tokenless() {

local pkg_name
pkg_name=$(parse_spec_name "$spec_in")

# Component contract guard (mirrors build_cosh_ng): the RPM must publish
# the anolisa-component(<name>) capability so `anolisa install tokenless`
# can resolve the package via Provides when the component index is
# unavailable. Fail the build on spec drift instead of shipping a package
# that silently lacks the capability (GH-2836).
local component_in="${TOKEN_DIR}/.anolisa/component.toml.in"
if [ ! -f "$component_in" ]; then
err "Component contract template not found: $component_in"
return 1
fi
local component_name
component_name=$(awk '
$0 == "[component]" { in_component = 1; next }
in_component && /^\[/ { exit }
in_component && /^name = / {
value = $0
sub(/^name = "/, "", value)
sub(/"$/, "", value)
print value
exit
}
' "$component_in")
if [ "$component_name" != "$pkg_name" ]; then
err "tokenless identity mismatch: RPM name '${pkg_name}', component name '${component_name}'"
return 1
fi
if ! grep -Fqx "Provides: anolisa-component(${component_name})" "$spec_in"; then
err "tokenless spec must provide anolisa-component(${component_name})"
return 1
Comment on lines +493 to +495

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce the guard in the RPM-producing workflow

When tokenless RPMs are produced by the checked-in nightly pipeline, this guard never runs: .github/workflows/docker-nightly.yaml delegates the tokenless job to _rpm-build.yaml, which processes the archived spec and invokes rpmbuild directly. Consequently, removing the Provides line still lets that pipeline publish an RPM with the original defect; move or duplicate this assertion in _rpm-build.yaml (ideally verify the built RPM's capabilities) so the production artifact path is protected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — accepted. Verified the gap: docker-nightly.yaml delegates the rpm-tokenless job to _rpm-build.yaml, which processes the archived spec and invokes rpmbuild directly, so the guard in scripts/rpm-build.sh never runs on the nightly path.

Fixed in f359ab9 by duplicating the assertion into _rpm-build.yaml (gated on inputs.component == 'tokenless', other components unaffected) and adding the artifact capability verification:

  1. New Verify tokenless component contract step before Build RPM: derives the component name from .anolisa/component.toml.in in the source archive, verifies it matches the RPM package name, and requires the exact Provides: anolisa-component(<name>) line in the processed spec — same assertions as build_tokenless.
  2. Collect RPM artifacts now verifies each built RPM actually publishes the capability via rpm -qp --provides, catching a malformed Provides line that rpmbuild would silently drop (mirrors the existing agentsight verification hook).

Local verification: simulated the nightly spec processing with the real tokenless.spec.in + component.toml.in — happy path passes; removing the Provides line (the original defect scenario), a Name/component mismatch, and a missing contract template all fail the guard; built minimal test RPMs with/without the capability to confirm the artifact check accepts/rejects correctly; actionlint clean. CI re-runs on this push.

fi

local tarball_name="${pkg_name}-${version}.tar.gz"

# Step 1: Process spec template
Expand Down
Loading