Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .github/workflows/ci-windows-cross.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ on:
- 'internal/edgeagent/**'
- 'cmd/ongrid-edge*/**'
- '.github/workflows/ci-windows-cross.yml'
- 'scripts/ci-docs-only.py'
- 'scripts/check-issue-templates.rb'

permissions:
contents: read
Expand All @@ -32,15 +34,25 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 2

- name: Classify documentation changes
id: changes
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: python3 scripts/ci-docs-only.py

- name: Set up Go
if: steps.changes.outputs.docs_only != 'true'
id: go
uses: actions/setup-go@v6
with:
go-version: '1.25.x'
cache: false

- name: Restore and refresh Go cache
if: steps.changes.outputs.docs_only != 'true'
uses: actions/cache@v4
with:
path: |
Expand All @@ -53,6 +65,7 @@ jobs:
go-v1-ubuntu24-${{ runner.os }}-${{ runner.arch }}-${{ steps.go.outputs.go-version }}-windows-cross-

- name: go build (GOOS=windows)
if: steps.changes.outputs.docs_only != 'true'
env:
GOOS: windows
GOARCH: amd64
Expand All @@ -64,6 +77,7 @@ jobs:
go build ./cmd/ongrid-edge ./cmd/ongrid-edge-supervisor

- name: go vet (GOOS=windows)
if: steps.changes.outputs.docs_only != 'true'
env:
GOOS: windows
GOARCH: amd64
Expand All @@ -73,6 +87,7 @@ jobs:
go vet ./cmd/ongrid-edge ./cmd/ongrid-edge-supervisor

- name: go test (GOOS=windows, no-cgo)
if: steps.changes.outputs.docs_only != 'true'
env:
GOOS: windows
GOARCH: amd64
Expand Down
64 changes: 40 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Classify documentation changes
id: changes
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha }}
CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: python3 scripts/ci-docs-only.py

- name: Set up Go
Expand Down Expand Up @@ -84,10 +84,14 @@ jobs:
if: steps.changes.outputs.docs_only != 'true'
run: make test-k8s-capabilities

web-test:
name: web test + build
web-checks:
name: web (${{ matrix.target }})
runs-on: ubuntu-24.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
target: [test-web-ci-apm, test-web-ci-platform, build-web]

defaults:
run:
Expand All @@ -103,7 +107,7 @@ jobs:
id: changes
working-directory: .
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha }}
CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: python3 scripts/ci-docs-only.py

- name: Set up Node.js
Expand All @@ -119,30 +123,25 @@ jobs:
run: make test-commit-policy

- name: Install dependencies
if: steps.changes.outputs.docs_only != 'true'
if: steps.changes.outputs.docs_only != 'true' && matrix.target != 'build-web'
run: npm ci

- name: Test
- name: Run web check
if: steps.changes.outputs.docs_only != 'true'
# Keep DOM-heavy suites from competing for CPU within their test timeouts.
run: >-
npm test -- --run --no-file-parallelism
src/api/apm.test.ts
src/components/apm
src/pages/Apm
src/pages/Kubernetes.test.tsx
src/pages/Edges.test.tsx
src/pages/Dashboard.test.tsx
src/pages/settings/Upgrade.test.tsx
src/pages/settings/LLM.test.tsx
src/pages/settings/CommunicationProviders.test.tsx
src/components/icons/Provider.test.tsx
src/pages/kubernetes/edgeAttachments.test.ts
src/pages/kubernetes/registryCommands.test.ts
working-directory: .
run: make ${{ matrix.target }}

- name: Build
if: steps.changes.outputs.docs_only != 'true'
run: npm run build
web-test:
name: web test + build
needs: web-checks
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require all web checks to succeed
env:
WEB_RESULT: ${{ needs.web-checks.result }}
run: test "$WEB_RESULT" = success

deployment-test:
name: deployment manifests
Expand All @@ -152,33 +151,50 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 2

- name: Classify documentation changes
id: changes
env:
CI_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: python3 scripts/ci-docs-only.py

- name: Set up Helm
if: steps.changes.outputs.docs_only != 'true'
uses: azure/setup-helm@v5

- name: Verify documentation-only CI routing
run: make test-ci-docs-only

- name: Check release scripts
if: steps.changes.outputs.docs_only != 'true'
run: bash -n dist/package.sh dist/package-k8s-chart.sh dist/build-edge-attachments.sh deploy/install/install.sh deploy/install/upgrade.sh deploy/install/uninstall.sh deploy/install/public-url.sh deploy/install/data-permissions.sh deploy/install/apply-pending-upgrade.sh deploy/install/edge/fetch-edge-assets.sh deploy/install/edge/verify-edge-deps-archive.sh deploy/install/edge/edge-assets-lib.sh scripts/verify-cnb-compose-images.sh scripts/verify-release-images.sh scripts/publish-release-image.sh scripts/publish-release-image-platform.sh scripts/merge-release-image-manifest.sh scripts/test-publish-release-image.sh scripts/test-release-image-platform-publish.sh scripts/ensure-cnb-release.sh scripts/test-ensure-cnb-release.sh scripts/test-publish-cnb-release-attachments.sh scripts/test-verify-cnb-release-attachments.sh scripts/test-release-workflow.sh scripts/test-edge-assets.sh scripts/test-edge-assets-lib.sh scripts/test-edge-installer-connectivity-guard.sh scripts/test-edge-upgrade-hook-guard.sh scripts/test-public-url.sh scripts/test-upgrade-data-permissions.sh scripts/test-compose-release-package.sh scripts/test-apply-pending-upgrade.sh scripts/publish-helm-chart.sh scripts/test-publish-helm-chart.sh scripts/test-k8s-chart.sh

- name: Verify Edge installer connectivity guard
if: steps.changes.outputs.docs_only != 'true'
run: bash scripts/test-edge-installer-connectivity-guard.sh

- name: Verify Compose image routing
if: steps.changes.outputs.docs_only != 'true'
run: make verify-compose-images

- name: Verify release image helpers
if: steps.changes.outputs.docs_only != 'true'
run: make test-release-image-publish

- name: Verify release workflow orchestration
if: steps.changes.outputs.docs_only != 'true'
run: make test-release-workflow

- name: Verify Helm Chart publish helper
if: steps.changes.outputs.docs_only != 'true'
run: make test-publish-k8s-chart

- name: Verify Compose-only release package
if: steps.changes.outputs.docs_only != 'true'
run: make test-release-package

- name: Lint and render Kubernetes chart
if: steps.changes.outputs.docs_only != 'true'
run: make test-k8s-chart
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -886,5 +886,24 @@ test-commit-policy: ## Verify PR commit policy and bot feedback
node scripts/test-commit-policy.cjs

.PHONY: test-ci-docs-only
test-ci-docs-only: ## Verify documentation-only CI routing
test-ci-docs-only: ## Verify lightweight CI routing and the required web check
python3 scripts/test-ci-docs-only.py

.PHONY: check-issue-templates
check-issue-templates: ## Validate GitHub Issue Forms and chooser metadata
ruby scripts/check-issue-templates.rb

# Keep the two slow DOM suites (APM and Kubernetes) on separate CI runners.
.PHONY: test-web-ci-apm
test-web-ci-apm: ## Run the serial APM frontend CI tests (requires npm ci in web)
cd web && npm test -- --run --no-file-parallelism \
src/api/apm.test.ts src/components/apm src/pages/Apm

.PHONY: test-web-ci-platform
test-web-ci-platform: ## Run the serial platform frontend CI tests (requires npm ci in web)
cd web && npm test -- --run --no-file-parallelism \
src/pages/Kubernetes.test.tsx src/pages/Edges.test.tsx \
src/pages/Dashboard.test.tsx src/pages/settings/Upgrade.test.tsx \
src/pages/settings/LLM.test.tsx src/pages/settings/CommunicationProviders.test.tsx \
src/components/icons/Provider.test.tsx src/pages/kubernetes/edgeAttachments.test.ts \
src/pages/kubernetes/registryCommands.test.ts
68 changes: 68 additions & 0 deletions scripts/check-issue-templates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env ruby
# Validate the chooser and common Issue Form fields without installing dependencies.
# ponytail: extend these checks when templates adopt more field-specific options.
require 'yaml'
require 'uri'

def text!(value)
raise 'expected non-empty text' unless value.is_a?(String) && !value.strip.empty?
end

def boolean!(value)
raise 'expected a boolean' unless [true, false].include?(value)
end

files = Dir['.github/ISSUE_TEMPLATE/*.{yml,yaml}'].sort
files.each do |file|
begin
data = YAML.safe_load(File.read(file))
raise 'expected a YAML mapping' unless data.is_a?(Hash)
if File.basename(file).match?(/\Aconfig\.ya?ml\z/)
boolean!(data['blank_issues_enabled']) if data.key?('blank_issues_enabled')
links = data.fetch('contact_links', [])
raise 'contact_links must be a list' unless links.is_a?(Array)
links.each do |link|
%w[name url about].each { |key| text!(link.fetch(key)) }
uri = URI.parse(link['url'])
raise 'contact URL must use HTTP(S)' unless %w[http https].include?(uri.scheme) && uri.host
end
next
end

%w[name description].each { |key| text!(data.fetch(key)) }
body = data.fetch('body')
raise 'body must contain form fields' unless body.is_a?(Array) && body.any? { |field| field['type'] != 'markdown' }
ids = []
body.each do |field|
type = field.fetch('type')
raise 'unknown form field type' unless %w[markdown input textarea dropdown checkboxes upload].include?(type)
if field.key?('id')
id = field['id']
raise 'invalid or duplicate field id' unless id.is_a?(String) && id.match?(/\A[a-zA-Z0-9_-]+\z/) && !ids.include?(id)
ids << id
end
attributes = field.fetch('attributes')
text!(attributes.fetch(type == 'markdown' ? 'value' : 'label'))
validations = field.fetch('validations', {})
boolean!(validations['required']) if validations.key?('required')
boolean!(attributes['multiple']) if attributes.key?('multiple')
next unless %w[dropdown checkboxes].include?(type)

options = attributes.fetch('options')
raise 'options must be a non-empty list' unless options.is_a?(Array) && !options.empty?
labels = options.map do |option|
if type == 'checkboxes'
boolean!(option['required']) if option.key?('required')
option.fetch('label')
else
option
end
end
labels.each { |label| text!(label) }
raise 'duplicate options' unless labels.uniq == labels
end
rescue StandardError => error
abort "#{file}: #{error.message}"
end
end
puts "Issue template checks passed (#{files.length} YAML files)"
26 changes: 19 additions & 7 deletions scripts/ci-docs-only.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Keep heavy PR checks for everything outside the documentation allowlist."""
"""Keep heavy checks for changes outside known non-runtime documentation."""

import os
from pathlib import Path
Expand All @@ -8,17 +8,27 @@


def docs_only(paths):
root_docs = {"AGENTS.md", "README.md", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "SECURITY.md"}
prose = {".md", ".markdown", ".rst"}
assets = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", ".avif", ".pdf"}
readme_dirs = {"api", "db", "deploy", "deploy/install", "dist", "examples/apm-languages",
"tests/e2e", "web", "web/e2e"}
# Onboarding.tsx imports this guide as raw text; it changes the shipped UI.
runtime_docs = {"docs/guides/apm-configuration-files.md"}
# ponytail: known documentation locations only; add newly audited locations here.
return bool(paths) and all(
path in root_docs
or ("/" not in path and path.startswith("README_") and path.endswith(".md"))
or (path.startswith("docs/") and path.endswith(".md"))
path not in runtime_docs and (
("/" not in path and (Path(path).suffix in prose or path == "LICENSE"))
or (path.startswith("docs/") and Path(path).suffix in prose)
or (path.startswith("docs/assets/") and Path(path).suffix in assets)
or (str(Path(path).parent) in readme_dirs and re.fullmatch(r"README(?:[._-][^/]*)?\.md", Path(path).name))
or re.fullmatch(r"\.github/(?:[^/]+\.md|(?:ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE)/[^/]+\.md|ISSUE_TEMPLATE/[^/]+\.ya?ml)", path)
)
for path in paths
)


def changed_paths(base):
# An absent base (including main pushes) or unavailable history means full CI.
# An absent base, new branch, or unavailable history means full CI.
if not re.fullmatch(r"[0-9a-f]{40}", base):
return []
try:
Expand All @@ -33,14 +43,16 @@ def changed_paths(base):


def main():
# Validate metadata even when the PR qualifies for the lightweight path.
subprocess.run(["ruby", str(Path(__file__).with_name("check-issue-templates.rb"))], check=True)
base = os.environ.get("CI_BASE_SHA", "")
only_docs = docs_only(changed_paths(base))
if only_docs:
subprocess.run(["git", "diff", "--check", base, "HEAD", "--"], check=True)
result = f"docs_only={str(only_docs).lower()}\n"
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
output.write(result)
print("Documentation-only PR: whitespace checked; skipping Go/web builds and tests."
print("Documentation/template-only changes: metadata and whitespace checked; skipping heavy checks."
if only_docs else "Running full CI: changes are not documentation-only or base is unavailable.")


Expand Down
Loading
Loading