Merge pull request #1842 from rcourtman/maintainer/20260902T030649Z

Make release checks reliable and dialogs accessible
This commit is contained in:
pulse-triage[bot]
2026-09-02 06:04:27 +01:00
committed by GitHub
19 changed files with 255 additions and 130 deletions
+20
View File
@@ -9,6 +9,10 @@ on:
description: 'Version number (e.g., 4.30.0)'
required: true
type: string
expected_source_sha:
description: 'Exact 40-character commit SHA admitted for this release'
required: true
type: string
release_notes:
description: 'Release notes (markdown)'
required: true
@@ -108,6 +112,22 @@ jobs:
visual_capture_count: ${{ steps.visual_plan.outputs.capture_count }}
visual_comparison_tag: ${{ steps.visual_plan.outputs.comparison_tag }}
steps:
- name: Verify admitted source commit
env:
EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}
run: |
set -euo pipefail
if [[ ! "${EXPECTED_SOURCE_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::expected_source_sha must be an exact 40-character commit SHA"
exit 1
fi
if [[ "${GITHUB_SHA}" != "${EXPECTED_SOURCE_SHA}" || \
"${GITHUB_WORKFLOW_SHA}" != "${EXPECTED_SOURCE_SHA}" ]]; then
echo "::error::Release dispatch expected ${EXPECTED_SOURCE_SHA}, but GitHub resolved source ${GITHUB_SHA} and workflow ${GITHUB_WORKFLOW_SHA}."
exit 1
fi
echo "[OK] Release dispatch is bound to ${EXPECTED_SOURCE_SHA}"
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
+38 -16
View File
@@ -2454,13 +2454,17 @@ func TestRun_DockerRetry(t *testing.T) {
origDocker := newDockerAgent
defer func() { newDockerAgent = origDocker }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
retrySucceeded := make(chan struct{})
// First call fails, second succeeds
calls := 0
var calls atomic.Int32
newDockerAgent = func(cfg dockeragent.Config) (RunnableCloser, error) {
calls++
if calls == 1 {
if calls.Add(1) == 1 {
return nil, errors.New("not available yet")
}
close(retrySucceeded)
return &mockRunnableCloser{mockRunnable: mockRunnable{started: make(chan struct{})}}, nil
}
@@ -2469,8 +2473,6 @@ func TestRun_DockerRetry(t *testing.T) {
retryInitialDelay = 1 * time.Millisecond
defer func() { retryInitialDelay = origInitial }()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
server := httptest.NewServer(http.NotFoundHandler())
defer server.Close()
@@ -2479,17 +2481,26 @@ func TestRun_DockerRetry(t *testing.T) {
errCh <- run(ctx, []string{"-token", "T", "-url", server.URL, "-enable-docker=true", "-enable-host=false"}, func(s string) string { return "" })
}()
select {
case <-retrySucceeded:
cancel()
case err := <-errCh:
t.Fatalf("run returned before Docker retry succeeded: %v", err)
case <-ctx.Done():
t.Fatalf("Docker retry did not succeed: %v", ctx.Err())
}
select {
case err := <-errCh:
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
case <-time.After(3 * time.Second):
t.Fatal("timeout waiting for run")
t.Fatal("timeout waiting for run to stop")
}
if calls < 2 {
t.Errorf("expected at least 2 calls to newDockerAgent, got %d", calls)
if got := calls.Load(); got != 2 {
t.Errorf("newDockerAgent calls = %d, want 2", got)
}
}
@@ -2547,13 +2558,17 @@ func TestRun_KubeRetry(t *testing.T) {
origKube := newKubeAgent
defer func() { newKubeAgent = origKube }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
retrySucceeded := make(chan struct{})
// First call fails, second succeeds
calls := 0
var calls atomic.Int32
newKubeAgent = func(cfg kubernetesagent.Config) (Runnable, error) {
calls++
if calls == 1 {
if calls.Add(1) == 1 {
return nil, errors.New("not available yet")
}
close(retrySucceeded)
return &mockRunnable{started: make(chan struct{})}, nil
}
@@ -2562,8 +2577,6 @@ func TestRun_KubeRetry(t *testing.T) {
retryInitialDelay = 1 * time.Millisecond
defer func() { retryInitialDelay = origInitial }()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
server := httptest.NewServer(http.NotFoundHandler())
defer server.Close()
@@ -2573,17 +2586,26 @@ func TestRun_KubeRetry(t *testing.T) {
errCh <- run(ctx, []string{"-token", "T", "-url", server.URL, "-enable-kubernetes=true", "-enable-host=false", "-enable-docker=false"}, func(s string) string { return "" })
}()
select {
case <-retrySucceeded:
cancel()
case err := <-errCh:
t.Fatalf("run returned before Kubernetes retry succeeded: %v", err)
case <-ctx.Done():
t.Fatalf("Kubernetes retry did not succeed: %v", ctx.Err())
}
select {
case err := <-errCh:
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
case <-time.After(3 * time.Second):
t.Fatal("timeout waiting for run")
t.Fatal("timeout waiting for run to stop")
}
if calls < 2 {
t.Errorf("expected at least 2 calls to newKubeAgent, got %d", calls)
if got := calls.Load(); got != 2 {
t.Errorf("newKubeAgent calls = %d, want 2", got)
}
}
@@ -2138,6 +2138,13 @@ artifact-selection behaviour.
trigger, promotion resolver, rendered release body, current upgrade guide,
or current release packet that routes systemd/LXC rollback through the
Unified Agent installer, and must retain explicit Docker image guidance.
17. Bind every publishing release dispatch to the exact commit admitted by the
caller. `.github/workflows/create-release.yml` must require a full
40-character `expected_source_sha` and, before checkout, reject the run
unless both `GITHUB_SHA` and `GITHUB_WORKFLOW_SHA` equal that commit.
`scripts/trigger-release.sh` and `scripts/trigger-stable-patch.sh` must send
the exact remote candidate SHA they already verified; branch ancestry or a
later branch tip is not equivalent release admission.
## Current State
@@ -41,6 +41,10 @@ overlay's accessible heading. A reusable panel may suppress its standalone
title when the owning overlay supplies the canonical title, while preserving
that title in inline and desktop contexts; the overlay remains responsible for
one visible heading, its accessible label, dismissal, and focus return.
The shared `Dialog` component requires exactly one accessible-name strategy at
its component boundary: consumers provide either `ariaLabelledBy` for a visible
heading or `ariaLabel` when no visible label is available. Unnamed dialogs and
consumers that provide both strategies must fail the frontend type boundary.
The alert schedule's initial-delivery selector composes `SettingsPanel` and
`FormSelect`, uses the shared alert-configuration presentation vocabulary, and
exposes the same email, webhook, Apprise, and all-destination labels used by
+25 -44
View File
@@ -1,42 +1,19 @@
{
"version": 1,
"base_sha": "2f8a4ec629b75c7ecefd38a9abc4b1511bc9a891",
"verified_at": "2026-09-01T19:54:19Z",
"base_sha": "9fba43ffed507f092f876acaf23bef013f0f6fab",
"verified_at": "2026-09-02T03:39:02Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/App.tsx",
"frontend-modern/src/AppLayout.tsx",
"frontend-modern/src/components/shared/MobileNavBar.tsx",
"frontend-modern/src/components/shared/mobileNavBarModel.ts",
"frontend-modern/src/features/home/HomePageSurface.tsx",
"frontend-modern/src/features/home/homePageModel.ts",
"frontend-modern/src/i18n/messages.de.ts",
"frontend-modern/src/i18n/messages.es.ts",
"frontend-modern/src/i18n/messages.ts",
"frontend-modern/src/routing/navigation.ts",
"frontend-modern/src/routing/resourceLinks.ts",
"frontend-modern/src/routing/routePreload.ts",
"frontend-modern/src/utils/assistantPageContext.ts"
],
"changed_paths": ["frontend-modern/src/components/shared/Dialog.tsx"],
"content_sha256": {
"frontend-modern/src/App.tsx": "d5473f7838148edeaaf5564f01c166d97b6086232dcba70f6296b5c58e563a4b",
"frontend-modern/src/AppLayout.tsx": "db685903fef2a1509edd812acb3904f7ff3ffab0bbd1af2bff183c1b3ae8e373",
"frontend-modern/src/components/shared/MobileNavBar.tsx": "27358f3e77267cc111ce97b32d715b0069e2d1370ddd2b3ecc4f2d34a3e6dac5",
"frontend-modern/src/components/shared/mobileNavBarModel.ts": "ab9e69d379579d02e33a2abd1224d8c13e6543dd6aa0ad40cc1f65697a289d04",
"frontend-modern/src/features/home/HomePageSurface.tsx": "deleted",
"frontend-modern/src/features/home/homePageModel.ts": "deleted",
"frontend-modern/src/i18n/messages.de.ts": "602246d3d4ce11a1a8a914027d950a30ba1f2f25850c385836013853957c2d0c",
"frontend-modern/src/i18n/messages.es.ts": "ed5a749603efad29293cd6964215f6edc21a4ce14bca99a48429ddd120770263",
"frontend-modern/src/i18n/messages.ts": "43a757e00eaa7879c400895c9c59a930e31513dbf03e0ae6d60f7e72a6ad9962",
"frontend-modern/src/routing/navigation.ts": "8ae1ad012e60ef345ffb3d18bb66f5d8af056758109655a8ec362ebf4c7b9556",
"frontend-modern/src/routing/resourceLinks.ts": "dee9a426de785e23390ba49c9067f55c8f923cecfce1247ff18f9b1004e0cc90",
"frontend-modern/src/routing/routePreload.ts": "ee79d423db0afcf8d76d1d39da59a13cb2c98e908e516a016cbb425982b25873",
"frontend-modern/src/utils/assistantPageContext.ts": "bfca19b4e183777ee1535073d48cce92ce31980f4105bab337f33a28538a6a35"
"frontend-modern/src/components/shared/Dialog.tsx": "185f09e185a9e5ccddf73906a81552ea0cc8b07390fb5ab587fbe1b952697735"
},
"routes": [
"/",
"/home",
"/proxmox/overview"
"/settings/infrastructure",
"/actions",
"/alerts/overview",
"/settings/system-general",
"/patrol",
"/"
],
"viewports": [
{
@@ -44,21 +21,25 @@
"height": 720
},
{
"width": 375,
"height": 812
"width": 390,
"height": 844
},
{
"width": 393,
"height": 851
}
],
"states": [
"signed-in desktop shell with primary tabs Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines and no Home tab",
"/home renders the Page Not Found surface with 'No route matched /home' and a Go to workspace button",
"signed-in narrow shell on /proxmox/overview with the bottom navigation bar showing Proxmox, Alerts, Patrol, Actions, More and no Home entry",
"More navigation sheet open at narrow width listing Settings only",
"no horizontal page overflow at 375px; no console errors at either width"
"Add infrastructure dialog open at desktop and narrow widths with reduced motion, a visible labelled heading, accessible description, focused close control, contained panel geometry, and no horizontal document overflow",
"Add infrastructure dialog dismissed at desktop and narrow widths with the underlying Infrastructure surface restored",
"authenticated Actions, Alerts, Infrastructure, General Settings, and Patrol surfaces with reduced motion and no automatically detectable WCAG A/AA violations",
"logged-out welcome surface with reduced motion and no automatically detectable WCAG A/AA violations"
],
"interactions": [
"navigated to / and /home at desktop width and read the rendered nav and main headings",
"pressed Go to workspace on the /home not-found page and landed on /proxmox/overview",
"resized to 375x812, loaded /proxmox/overview, opened the More navigation sheet, closed it with Escape",
"confirmed the default landing route and nav order are unchanged from the parent revision apart from the removed Home entry"
"opened Add infrastructure from its named trigger at desktop and narrow widths and verified the dialog accessible name and description",
"inspected final desktop and 390x844 screenshots for placement, clipping, stacking, scrolling, focus treatment, and responsive layout",
"verified the dialog bounds stay inside both viewports and the document has no horizontal overflow",
"dismissed the dialog with Escape at desktop and narrow widths and verified focus returned to Add infrastructure",
"scanned representative authenticated and logged-out surfaces for WCAG A/AA violations and unexpected reduced-motion effects"
]
}
+5 -1
View File
@@ -258,6 +258,7 @@ export function AppLayout(props: AppLayoutProps) {
const [skipLinkFocused, setSkipLinkFocused] = createSignal(false);
const [primaryRouteMemoryVersion, setPrimaryRouteMemoryVersion] = createSignal(0);
let headerEl: HTMLDivElement | undefined;
let mainContentEl: HTMLElement | undefined;
let assistantLauncherEl: HTMLButtonElement | undefined;
let restoreAssistantLauncherFocus = false;
let headerHideTimeout: ReturnType<typeof setTimeout> | undefined;
@@ -730,6 +731,7 @@ export function AppLayout(props: AppLayoutProps) {
jump past the chrome straight into the page content. */}
<a
href="#main"
onClick={() => mainContentEl?.focus()}
onFocus={() => setSkipLinkFocused(true)}
onBlur={() => setSkipLinkFocused(false)}
class={
@@ -814,7 +816,7 @@ export function AppLayout(props: AppLayoutProps) {
</span>
</Show>
<Show when={props.versionInfo()?.channel === 'rc'}>
<span class="text-xs px-1.5 py-0.5 bg-orange-500 text-white rounded font-bold">
<span class="text-xs px-1.5 py-0.5 bg-orange-700 text-white rounded font-bold">
Preview
</span>
</Show>
@@ -980,7 +982,9 @@ export function AppLayout(props: AppLayoutProps) {
</Show>
<main
ref={mainContentEl}
id="main"
tabindex="-1"
class="tab-content mb-1 block rounded-b rounded-tl rounded-tr bg-surface shadow sm:mb-2"
>
<div class="pulse-panel">
@@ -174,7 +174,16 @@ describe('AppLayout navigation icons', () => {
expect(container.querySelector('.pulse-shell')).toHaveClass('pb-safe-or-14');
expect(container.querySelector('.pulse-shell')).not.toHaveClass('pb-safe-or-16');
expect(container.querySelector('.header')).toHaveClass('mb-1', 'sm:mb-3');
expect(container.querySelector('main')).toHaveClass('mb-1', 'sm:mb-2');
const main = container.querySelector('main');
expect(main).toHaveClass('mb-1', 'sm:mb-2');
expect(main).toHaveAttribute('id', 'main');
expect(main).toHaveAttribute('tabindex', '-1');
const skipLink = screen.getByRole('link', { name: 'Skip to main content' });
expect(skipLink).toHaveAttribute('href', '#main');
fireEvent.click(skipLink);
expect(main).toHaveFocus();
expect(screen.getByText('Preview')).toHaveClass('bg-orange-700', 'text-white');
expect(screen.getByText('Preview')).not.toHaveClass('bg-orange-500');
expect(container.querySelector('footer')).toHaveClass('pulse-footer', 'px-2', 'sm:px-4');
const desktopNav = screen.getByRole('navigation', { name: 'Primary navigation' });
@@ -223,6 +223,7 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
<Toggle
checked={layoutStore.isFullWidth()}
class="shrink-0"
ariaLabel={t('settings.general.fullWidth.title')}
onChange={() => layoutStore.toggle()}
/>
</div>
@@ -276,6 +277,7 @@ export const GeneralSettingsPanel: Component<GeneralSettingsPanelProps> = (props
<Toggle
checked={props.telemetryEnabled()}
class="shrink-0"
ariaLabel={t('settings.general.telemetry.title')}
disabled={props.telemetryEnabledLocked() || props.savingTelemetry()}
onChange={() => props.handleTelemetryEnabledChange(!props.telemetryEnabled())}
/>
@@ -86,6 +86,14 @@ describe('GeneralSettingsPanel localization', () => {
).toBeInTheDocument();
expect(screen.getByText('Usage data and privacy')).toBeInTheDocument();
expect(screen.getByText('Outbound usage telemetry')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Full-width mode' })).toHaveAttribute(
'aria-pressed',
'false',
);
expect(screen.getByRole('button', { name: 'Outbound usage telemetry' })).toHaveAttribute(
'aria-pressed',
'true',
);
expect(screen.getByRole('button', { name: 'Preview payload' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Reset ID' })).toBeInTheDocument();
expect(screen.getByText('Monitoring cadence')).toBeInTheDocument();
@@ -9,19 +9,29 @@ import {
} from './dialogModel';
import { useDialogState } from './useDialogState';
interface DialogProps {
interface DialogBaseProps {
isOpen: boolean;
onClose: () => void;
children: JSX.Element;
panelClass?: string;
layout?: DialogLayout;
closeOnBackdrop?: boolean;
ariaLabel?: string;
ariaLabelledBy?: string;
ariaDescribedBy?: string;
returnFocus?: () => HTMLElement | null | undefined;
}
type DialogProps = DialogBaseProps &
(
| {
ariaLabel: string;
ariaLabelledBy?: never;
}
| {
ariaLabel?: never;
ariaLabelledBy: string;
}
);
export const Dialog: Component<DialogProps> = (props) => {
const state = useDialogState(props);
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { createSignal, Show } from 'solid-js';
import type { ComponentProps, JSX } from 'solid-js';
import { Dialog } from '@/components/shared/Dialog';
import { dialogStackHasBlockingDialog } from '@/components/shared/useDialogState';
import dialogSource from '@/components/shared/Dialog.tsx?raw';
@@ -53,10 +54,30 @@ describe('Dialog', () => {
expect(dialogModelSource).toContain('FOCUSABLE_SELECTOR');
});
it('requires exactly one accessible-name strategy at the component boundary', () => {
type UnnamedDialogProps = {
isOpen: boolean;
onClose: () => void;
children: JSX.Element;
};
type DialogComponentProps = ComponentProps<typeof Dialog>;
expectTypeOf<UnnamedDialogProps>().not.toMatchTypeOf<DialogComponentProps>();
expectTypeOf<
UnnamedDialogProps & { ariaLabel: string }
>().toMatchTypeOf<DialogComponentProps>();
expectTypeOf<
UnnamedDialogProps & { ariaLabelledBy: string }
>().toMatchTypeOf<DialogComponentProps>();
expectTypeOf<
UnnamedDialogProps & { ariaLabel: string; ariaLabelledBy: string }
>().not.toMatchTypeOf<DialogComponentProps>();
});
it('renders as a modal dialog and closes on backdrop click', () => {
const onClose = vi.fn();
render(() => (
<Dialog isOpen={true} onClose={onClose}>
<Dialog isOpen={true} onClose={onClose} ariaLabel="Test dialog">
<div class="p-4">
<button type="button">Action</button>
</div>
@@ -76,7 +97,7 @@ describe('Dialog', () => {
it('closes on Escape and locks body scroll while open', () => {
const onClose = vi.fn();
const { unmount } = render(() => (
<Dialog isOpen={true} onClose={onClose}>
<Dialog isOpen={true} onClose={onClose} ariaLabel="Test dialog">
<div class="p-4">Body</div>
</Dialog>
));
@@ -96,7 +117,7 @@ describe('Dialog', () => {
expect(dialogStackHasBlockingDialog()).toBe(false);
const { unmount } = render(() => (
<Dialog isOpen={true} onClose={() => undefined}>
<Dialog isOpen={true} onClose={() => undefined} ariaLabel="Test dialog">
<div class="p-4">Body</div>
</Dialog>
));
@@ -112,7 +133,7 @@ describe('Dialog', () => {
document.body.appendChild(background);
const { unmount } = render(() => (
<Dialog isOpen={true} onClose={() => undefined}>
<Dialog isOpen={true} onClose={() => undefined} ariaLabel="Test dialog">
<button type="button">Dialog action</button>
</Dialog>
));
@@ -133,7 +154,7 @@ describe('Dialog', () => {
document.body.appendChild(background);
const { unmount } = render(() => (
<Dialog isOpen={true} onClose={() => undefined}>
<Dialog isOpen={true} onClose={() => undefined} ariaLabel="Test dialog">
<button type="button">Dialog action</button>
</Dialog>
));
@@ -178,7 +199,7 @@ describe('Dialog', () => {
it('makes body-level surfaces added while a dialog is open inert', async () => {
render(() => (
<Dialog isOpen={true} onClose={() => undefined}>
<Dialog isOpen={true} onClose={() => undefined} ariaLabel="Test dialog">
<button type="button">Dialog action</button>
</Dialog>
));
@@ -194,7 +215,7 @@ describe('Dialog', () => {
it('keeps keyboard focus trapped in the dialog', async () => {
const onClose = vi.fn();
render(() => (
<Dialog isOpen={true} onClose={onClose}>
<Dialog isOpen={true} onClose={onClose} ariaLabel="Test dialog">
<div class="p-4">
<button type="button">First</button>
<button type="button">Last</button>
@@ -294,7 +315,7 @@ describe('Dialog', () => {
it('honors an explicitly requested initial focus target', async () => {
render(() => (
<Dialog isOpen={true} onClose={() => undefined}>
<Dialog isOpen={true} onClose={() => undefined} ariaLabel="Test dialog">
<div class="p-4">
<button type="button">Close</button>
<textarea aria-label="Outcome" autofocus />
@@ -313,7 +334,7 @@ describe('Dialog', () => {
<button type="button" onClick={() => setIsOpen(true)}>
Open investigation
</button>
<Dialog isOpen={isOpen()} onClose={() => setIsOpen(false)}>
<Dialog isOpen={isOpen()} onClose={() => setIsOpen(false)} ariaLabel="Investigation">
<button type="button" onClick={() => setIsOpen(false)}>
Close investigation
</button>
@@ -347,6 +368,7 @@ describe('Dialog', () => {
</Show>
<Dialog
isOpen={isOpen()}
ariaLabel="Remove item"
onClose={() => {
setShowTrigger(false);
setIsOpen(false);
@@ -0,0 +1,64 @@
//go:build linux || darwin || freebsd
package installtests
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
func TestInstallSHAgentIDRecoveryRejectsSymlinkFIFOAndOversizedState(t *testing.T) {
binaryPath := buildLifecycleAgent(t)
root := t.TempDir()
validPath := filepath.Join(root, "valid-agent-id")
oversizedPath := filepath.Join(root, "oversized-agent-id")
symlinkPath := filepath.Join(root, "symlink-agent-id")
fifoPath := filepath.Join(root, "fifo-agent-id")
if err := os.WriteFile(validPath, []byte("agent-safe-123\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(oversizedPath, []byte(strings.Repeat("a", 5000)), 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(validPath, symlinkPath); err != nil {
t.Fatal(err)
}
if err := syscall.Mkfifo(fifoPath, 0600); err != nil {
t.Fatal(err)
}
harness := func(path string) ([]byte, error) {
script := `
set -euo pipefail
COLLECTOR_LIFECYCLE_BINARY_PATH="` + binaryPath + `"
INSTALL_DIR="` + root + `"
BINARY_NAME="pulse-agent"
LEAST_PRIVILEGE_USER="pulse-agent-test-missing"
` + extractLifecycleTrustShellFunctions(t) + `
` + extractInstallShellFunction(t, "collector_lifecycle_binary") + `
` + extractInstallShellFunction(t, "read_agent_id_file_safely") + `
read_agent_id_file_safely "` + path + `"
`
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return exec.CommandContext(ctx, "bash", "-c", script).CombinedOutput()
}
if out, err := harness(validPath); err != nil || strings.TrimSpace(string(out)) != "agent-safe-123" {
t.Fatalf("valid descriptor-bound agent ID recovery failed: %v\n%s", err, out)
}
for _, path := range []string{symlinkPath, fifoPath, oversizedPath} {
started := time.Now()
if out, err := harness(path); err == nil {
t.Fatalf("unsafe agent ID path %s was accepted:\n%s", path, out)
}
if elapsed := time.Since(started); elapsed >= 2*time.Second {
t.Fatalf("unsafe agent ID path %s blocked for %s", path, elapsed)
}
}
}
@@ -5,7 +5,6 @@ package installtests
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"io"
"net/http"
@@ -21,59 +20,6 @@ import (
"time"
)
func TestInstallSHAgentIDRecoveryRejectsSymlinkFIFOAndOversizedState(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Unix descriptor-bound identity recovery")
}
binaryPath := buildLifecycleAgent(t)
root := t.TempDir()
validPath := filepath.Join(root, "valid-agent-id")
oversizedPath := filepath.Join(root, "oversized-agent-id")
symlinkPath := filepath.Join(root, "symlink-agent-id")
fifoPath := filepath.Join(root, "fifo-agent-id")
if err := os.WriteFile(validPath, []byte("agent-safe-123\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(oversizedPath, []byte(strings.Repeat("a", 5000)), 0600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(validPath, symlinkPath); err != nil {
t.Fatal(err)
}
if err := syscall.Mkfifo(fifoPath, 0600); err != nil {
t.Fatal(err)
}
harness := func(path string) ([]byte, error) {
script := `
set -euo pipefail
COLLECTOR_LIFECYCLE_BINARY_PATH="` + binaryPath + `"
INSTALL_DIR="` + root + `"
BINARY_NAME="pulse-agent"
LEAST_PRIVILEGE_USER="pulse-agent-test-missing"
` + extractLifecycleTrustShellFunctions(t) + `
` + extractInstallShellFunction(t, "collector_lifecycle_binary") + `
` + extractInstallShellFunction(t, "read_agent_id_file_safely") + `
read_agent_id_file_safely "` + path + `"
`
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
return exec.CommandContext(ctx, "bash", "-c", script).CombinedOutput()
}
if out, err := harness(validPath); err != nil || strings.TrimSpace(string(out)) != "agent-safe-123" {
t.Fatalf("valid descriptor-bound agent ID recovery failed: %v\n%s", err, out)
}
for _, path := range []string{symlinkPath, fifoPath, oversizedPath} {
started := time.Now()
if out, err := harness(path); err == nil {
t.Fatalf("unsafe agent ID path %s was accepted:\n%s", path, out)
}
if elapsed := time.Since(started); elapsed >= 2*time.Second {
t.Fatalf("unsafe agent ID path %s blocked for %s", path, elapsed)
}
}
}
type agentLifecycleControlPlane struct {
mu sync.Mutex
online bool
@@ -933,6 +933,10 @@ func TestCreateReleaseUploadsPowerShellInstaller(t *testing.T) {
convergenceWorkflow := string(convergenceContent)
required := []string{
`historical_asset_backfill_only:`,
`expected_source_sha:`,
`EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}`,
`"${GITHUB_SHA}" != "${EXPECTED_SOURCE_SHA}"`,
`"${GITHUB_WORKFLOW_SHA}" != "${EXPECTED_SOURCE_SHA}"`,
`description: 'Repair an already-published release packet in place without rebuilding binaries'`,
`SYFT_VERSION="1.42.4"`,
`SYFT_ARCHIVE="syft_${SYFT_VERSION}_linux_amd64.tar.gz"`,
@@ -1689,6 +1689,10 @@ class ReleasePromotionPolicyTest(unittest.TestCase):
self.assertIn("build_rollback_section", renderer)
self.assertIn("promotion metadata out of customer notes", renderer)
self.assertIn("historical_asset_backfill_only:", content)
self.assertIn("expected_source_sha:", content)
self.assertIn('EXPECTED_SOURCE_SHA: ${{ inputs.expected_source_sha }}', content)
self.assertIn('"${GITHUB_SHA}" != "${EXPECTED_SOURCE_SHA}"', content)
self.assertIn('"${GITHUB_WORKFLOW_SHA}" != "${EXPECTED_SOURCE_SHA}"', content)
self.assertIn("Repair an already-published release packet in place without rebuilding binaries", content)
self.assertIn("draft: true", content)
self.assertIn("activate_release:", content)
@@ -11,6 +11,7 @@
"pkg/agents/docker/report_limits.go",
"scripts/install.sh",
"scripts/release_ldflags.sh",
"scripts/installtests/agent_id_recovery_unix_test.go",
"scripts/installtests/agent_state_dir_lifecycle_test.go",
"scripts/installtests/backfill_release_assets_test.go",
"scripts/installtests/build_release_assets_test.go",
+3
View File
@@ -120,6 +120,7 @@ python3 scripts/check-workflow-dispatch-inputs.py \
--workflow-path .github/workflows/create-release.yml \
--branch "$CURRENT_BRANCH" \
--require version \
--require expected_source_sha \
--require release_notes \
--require release_screenshot_plan \
--require promoted_from_tag \
@@ -372,6 +373,7 @@ echo "Triggering release workflow..."
if [ -n "$NOTES_FILE" ]; then
jq -n \
--arg version "$VERSION" \
--arg expected_source_sha "$LOCAL" \
--rawfile release_notes "$NOTES_FILE" \
--rawfile release_screenshot_plan "$VISUAL_PLAN_FILE" \
--arg rollback_version "$ROLLBACK_VERSION" \
@@ -387,6 +389,7 @@ if [ -n "$NOTES_FILE" ]; then
--arg mobile_release_evidence "$MOBILE_RELEASE_EVIDENCE" \
'{
version: $version,
expected_source_sha: $expected_source_sha,
release_notes: $release_notes,
release_screenshot_plan: $release_screenshot_plan,
rollback_version: $rollback_version,
+3
View File
@@ -203,6 +203,7 @@ else
--workflow-path .github/workflows/create-release.yml \
--branch "$CURRENT_BRANCH" \
--require version \
--require expected_source_sha \
--require release_notes \
--require release_screenshot_plan \
--require promoted_from_tag \
@@ -219,6 +220,7 @@ else
jq -n \
--arg version "$VERSION" \
--arg expected_source_sha "$LOCAL_SHA" \
--rawfile release_notes "$NOTES_FILE" \
--rawfile release_screenshot_plan "$VISUAL_PLAN_FILE" \
--arg promoted_from_tag "" \
@@ -234,6 +236,7 @@ else
--arg mobile_release_evidence "$MOBILE_RELEASE_EVIDENCE" \
'{
version: $version,
expected_source_sha: $expected_source_sha,
release_notes: $release_notes,
release_screenshot_plan: $release_screenshot_plan,
promoted_from_tag: $promoted_from_tag,
@@ -137,8 +137,19 @@ test("Actions remains named, directly reachable, keyboard accessible, and free o
document.documentElement.clientWidth,
);
expect(overflow).toBeFalsy();
const skipLink = page.getByRole("link", { name: "Skip to main content" });
await page.evaluate(() => {
document.body.tabIndex = -1;
document.body.focus();
document.body.removeAttribute("tabindex");
});
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
await expect(skipLink).toBeFocused();
await expect(skipLink).toBeVisible();
await page.keyboard.press("Enter");
await expect(page.locator("#main")).toBeFocused();
await testInfo.attach("actions-phone-width", {
body: await page.screenshot(),
contentType: "image/png",