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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Shared standards validation

on:
push:
branches: [master]
pull_request:

permissions:
contents: read

jobs:
policy:
uses: ./.github/workflows/policy.yml
with:
allow-terms: |
ChatGPT
Codex
Cursor
Claude

result:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install the workflow validator
env:
GOBIN: ${{ runner.temp }}/workflow-tools
run: |
go install github.com/rhysd/actionlint/cmd/[email protected]
echo "$GOBIN" >> "$GITHUB_PATH"
- name: Validate shared standards
run: Scripts/check
167 changes: 167 additions & 0 deletions .github/workflows/policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Generated from Scripts/check-policy.py by Scripts/sync-policy-workflow.py.
name: Source policy

on:
workflow_call:
inputs:
allow-terms:
description: Newline-separated product names required by this repository
type: string
default: ''
allow-bots:
description: Newline-separated GitHub bot logins permitted for commit identities
type: string
default: ''

permissions:
contents: read

jobs:
policy:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Check source identity, content and version
env:
GH_TOKEN: ${{ github.token }}
ALLOW_TERMS: ${{ inputs.allow-terms }}
ALLOW_BOTS: ${{ inputs.allow-bots }}
run: |
python3 - <<'PY'
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
"""Validate the source range of a pull request or default-branch push."""

import json
import os
from pathlib import Path
import re
import subprocess
import sys

OWNER_EMAIL = "[email protected]"
ZERO_SHA = "0" * 40
TRAILER = re.compile(
r"^(Co-authored-by|Made-with|Generated-by|Generated-with|Assisted-by|Signed-off-by):"
r"|Generated with|cursor\.com|anthropic\.com|claude\.ai|openai\.com", re.I | re.M)
PRIVATE_PATH = re.compile(r"/(?:Users|home)/[^/\s]+/|\.(?:agent|workspace)/")
PLAN = re.compile(r"(?:\b[^\s/]+\.plan\.md\b|\bPLANS\.md\b)", re.I)
AGENT = re.compile(r"\b(ChatGPT|Codex|Cursor|Claude)\b", re.I)


def git(*arguments):
return subprocess.check_output(["git", *arguments], text=True, stdin=subprocess.DEVNULL)


def api_commit(repository, sha):
return json.loads(subprocess.check_output(
["gh", "api", f"repos/{repository}/commits/{sha}"], text=True))


def commit_findings(sha, record, allow_bots):
commit = record["commit"]
findings = []
for role in ("author", "committer"):
email = commit[role]["email"].lower()
login = (record.get(role) or {}).get("login", "").lower()
allowed = email == OWNER_EMAIL or login in allow_bots
if role == "committer" and email == "[email protected]":
allowed = True
if not allowed:
findings.append(f"{sha}: unexpected {role} identity")
if not commit.get("verification", {}).get("verified", False):
findings.append(f"{sha}: commit is not Verified by GitHub")
if TRAILER.search(commit["message"]):
findings.append(f"{sha}: attribution trailer or tool attribution in commit message")
return findings


def content_findings(path, number, text, allow_terms):
reasons = []
if PRIVATE_PATH.search(text):
reasons.append("private execution path")
if PLAN.search(text):
reasons.append("private plan file")
terms = sorted({match.group() for match in AGENT.finditer(text)
if match.group().lower() not in allow_terms})
if terms:
reasons.append("unapproved product/tool term: " + ", ".join(terms))
return [f"{path}:{number}: {reason}" for reason in reasons]


def added_lines(patch):
number = None
for line in patch.splitlines():
match = re.match(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", line)
if match:
number = int(match.group(1))
elif number is not None:
if line.startswith("+"):
yield number, line[1:]
number += 1
elif line.startswith(" "):
number += 1


def source_range(event, name):
if name == "pull_request":
return event["pull_request"]["base"]["sha"], event["pull_request"]["head"]["sha"]
if name == "push" and not event.get("deleted", False):
return event["before"], event["after"]
raise ValueError("Policy requires a pull_request or non-deletion push event")


def check_range(repository, base, head, allow_terms, allow_bots, fetch_commit=api_commit):
for sha in (base, head):
if not re.fullmatch(r"[0-9a-f]{40}", sha):
raise ValueError("Event contains an invalid source SHA")
if head == ZERO_SHA:
raise ValueError("Policy requires a source commit")
new_branch = base == ZERO_SHA
revision = head if new_branch else f"{base}..{head}"
commits = git("rev-list", "--reverse", revision).splitlines()
findings = []
for sha in commits:
findings.extend(commit_findings(sha, fetch_commit(repository, sha), allow_bots))

if new_branch:
comparison = git("hash-object", "-t", "tree", "--stdin").strip()
else:
comparison = git("merge-base", base, head).strip()
paths = git("diff", "--name-only", "--no-renames", "-z", comparison, head).split("\0")
for path in filter(None, paths):
findings.extend(content_findings(path, 1, path, allow_terms))
patch = git("diff", "--no-ext-diff", "--no-color", "--no-renames", "--unified=0",
comparison, head, "--", path)
for number, line in added_lines(patch):
findings.extend(content_findings(path, number, line, allow_terms))
return findings


def main():
event = json.loads(Path(os.environ["GITHUB_EVENT_PATH"]).read_text())
base, head = source_range(event, os.environ["GITHUB_EVENT_NAME"])
allow_terms = {x.strip().lower() for x in os.environ.get("ALLOW_TERMS", "").splitlines() if x.strip()}
allow_bots = {x.strip().lower() for x in os.environ.get("ALLOW_BOTS", "").splitlines() if x.strip()}
findings = check_range(os.environ["GITHUB_REPOSITORY"], base, head, allow_terms, allow_bots)
if findings:
print("\n".join(findings))
return 1
validator = Path("Scripts/validate-version")
if validator.is_file():
subprocess.run([str(validator)], check=True)
print("policy ok")
return 0


if __name__ == "__main__":
try:
sys.exit(main())
except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error:
print(f"policy: {error}", file=sys.stderr)
sys.exit(1)
PY
125 changes: 124 additions & 1 deletion .github/workflows/swift-package-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ permissions:
contents: read

jobs:
result:
runs-on: ubuntu-24.04
needs: [configure, check]
if: always()
steps:
- name: Require all validation jobs to succeed
env:
RESULTS: ${{ toJSON(needs) }}
run: |
python3 - <<'PY'
import json, os
jobs = json.loads(os.environ['RESULTS'])
failed = [name for name, job in jobs.items() if job['result'] != 'success']
if failed:
raise SystemExit('Validation did not succeed: ' + ', '.join(failed))
print('All validation jobs passed')
PY

configure:
runs-on: ubuntu-24.04
outputs:
Expand Down Expand Up @@ -100,8 +118,113 @@ jobs:
set -euo pipefail
mkdir -p .build/release-validation
bash -eo pipefail -c "$CHECK_COMMAND" 2>&1 | tee .build/release-validation/check.log
- name: Preserve validation evidence
# Begin generated evidence gate.
- name: Check validation evidence
id: evidence
if: always()
run: |
python3 - <<'PY'
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
"""Normalize execution paths in logs and reject private context in uploadable text."""

import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import tarfile

# Product identifiers and source filenames are meaningful evidence, not attribution.
PRIVATE_PATH = re.compile(r"/(?:Users|home)/[^/\s]+/|\.(?:workspace|agent)/|(?:^|[\s/])(?:[^\s/]+\.plan\.md|PLANS\.md)(?:$|[\s:])")


def findings(data, name):
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
return []
return [f"{name}:{number}: private execution path or plan reference"
for number, line in enumerate(text.splitlines(), 1) if PRIVATE_PATH.search(line)]


def archive_findings(data, name, depth=0):
if depth > 4:
return [f"{name}: nested archive depth exceeds the evidence check limit"]
errors = []
with tarfile.open(fileobj=io.BytesIO(data), mode="r:*") as archive:
for member in archive:
label = name + "!" + member.name
path = PurePosixPath(member.name)
if path.is_absolute() or ".." in path.parts or member.issym() or member.islnk():
errors.append(f"{label}: unsafe archive member")
continue
if member.isfile():
content = archive.extractfile(member).read()
if member.name.endswith((".tar.gz", ".tgz", ".tar")):
errors.extend(archive_findings(content, label, depth + 1))
else:
errors.extend(findings(content, label))
return errors


def check(directory, replacements):
"""Normalize plain-text logs; scan UTF-8 files and compressed DocC contents."""
errors = []
if not directory.is_dir():
return ["Validation evidence directory is missing"]
replacements = sorted(((source, value) for source, value in replacements if len(source) > 1),
key=lambda pair: len(pair[0]), reverse=True)
for path in sorted(directory.rglob("*")):
name = path.relative_to(directory).as_posix()
if path.is_symlink():
errors.append(f"{name}: evidence must not contain symbolic links")
continue
if not path.is_file():
continue
content = path.read_bytes()
if path.suffix == ".log":
try:
text = content.decode("utf-8")
except UnicodeDecodeError:
errors.append(f"{name}: log is not UTF-8")
continue
for source, value in replacements:
text = text.replace(source, value)
content = text.encode("utf-8")
path.write_bytes(content)
if name == "package.json":
manifest = json.loads(content)
package_kind = manifest.get("packageKind", {})
if package_kind.get("root") == [str(Path.cwd())]:
package_kind["root"] = ["."]
content = (json.dumps(manifest, indent=2) + "\n").encode()
path.write_bytes(content)
errors.extend(findings(name.encode(), name))
if path.name.endswith((".tar.gz", ".tgz", ".tar")):
errors.extend(archive_findings(content, name))
else:
errors.extend(findings(content, name))
return errors


def main():
replacements = [(str(Path.cwd()), "<package>"), (str(Path.home()), "<home>")]
for key in ("RUNNER_TEMP", "TMPDIR"):
if os.environ.get(key):
replacements.append((os.environ[key].rstrip("/"), "<temporary>"))
errors = check(Path(".build/release-validation"), replacements)
if errors:
raise SystemExit("\n".join(errors))
print("validation evidence paths checked")


if __name__ == "__main__":
main()
PY
# End generated evidence gate.
- name: Preserve validation evidence
if: always() && steps.evidence.outcome == 'success'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: validation-${{ matrix.name }}
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.build/
__pycache__/
Loading