diff --git a/.github/workflows/_rpm-build.yaml b/.github/workflows/_rpm-build.yaml index e7f1986878..5ad5bcfaab 100644 --- a/.github/workflows/_rpm-build.yaml +++ b/.github/workflows/_rpm-build.yaml @@ -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()" + # 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 \ @@ -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: diff --git a/scripts/rpm-build.sh b/scripts/rpm-build.sh index 7544f05075..58e2fbc849 100755 --- a/scripts/rpm-build.sh +++ b/scripts/rpm-build.sh @@ -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() 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 + fi + local tarball_name="${pkg_name}-${version}.tar.gz" # Step 1: Process spec template