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
16 changes: 15 additions & 1 deletion .github/instructions/ci/workflow-contract.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ This is an interim compatibility measure for [#663](https://github.com/z-shell/.
## 3. Concurrency & Execution Control

- **Branch / PR Workflows**: Declare a `concurrency` block with `cancel-in-progress: true` to prevent resource waste and race conditions on rapid pushes.
- **Required checks with overlapping events**: When push and PR metadata events can produce checks for the same head, do not cancel or supersede those short runs. GitHub retains cancelled check results; even `cancel-in-progress: false` can replace a pending run. Omit concurrency for this documented case (z-shell/zi#597), and verify the effective required checks. A reusable workflow that owns concurrency must use a group distinct from its caller, or the caller must omit its own group.
- **Release / Deployment Workflows**: Set `cancel-in-progress: false` to ensure in-flight deployments complete deterministically.

---
Expand All @@ -89,6 +90,19 @@ This is an interim compatibility measure for [#663](https://github.com/z-shell/.
- Reference called workflows using pinned immutable refs.
- Expose job `outputs` cleanly for downstream dependent jobs (`needs:`).

The shared `zsh-ci.yml` owns native syntax and optional compilation. Callers
declare extensionless Zsh sources through newline-delimited `extra-files`,
set `require-sources: true` when an empty inventory is an error, and enable
`run-zcompile` for the organization baseline. Existing `*.zsh` discovery is
retained. Compilation uses disposable output outside the checkout. Consumers
own source classification, supported versions and functional tests.

The shared `codeql.yml` supports `workflow_call` for Actions
analysis; it does not analyze Zsh semantics. Both native Zsh and CodeQL accept
`checkout-ref` for an exact candidate SHA and use distinct concurrency groups
when called from the same parent workflow. Preserve required-check names and
prove caller coverage before changing repository rulesets.

---

## 5. Organization-Retired Patterns
Expand All @@ -110,7 +124,7 @@ new uses; migrate existing uses through their owning rollout and runbook.
- [ ] Filename is `kebab-case.yml` with appropriate category prefix.
- [ ] Workflow `name:` and Job `name:` contain NO emojis.
- [ ] Top-level `permissions:` is declared with minimum necessary scope.
- [ ] `concurrency:` block is present with `cancel-in-progress` set appropriately.
- [ ] Concurrency follows the execution-control rules above, including documented required-check exceptions and distinct caller/callee groups.
- [ ] All remote actions and reusable workflows are pinned to 40-character commit SHAs with associated release comments, or the documented interim `# main` comment for organization reusable workflows.
- [ ] Any interim pinact exception uses the shared template's exact repository/workflow and full-SHA match; the selected workflow and commit are verified separately.
- [ ] Actionlint and YAML syntax checks pass.
3 changes: 2 additions & 1 deletion .github/instructions/quality/testing.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ definitions come from `decisions/0007-release-publication-flow.md`.
## Baseline (every repo)

- Workflows follow org conventions: SHA-pinned actions, top-level
least-privilege `permissions:`, `concurrency:` on push/PR, no-emoji workflow/
least-privilege `permissions:`, push/PR concurrency with the workflow
contract's documented required-check exceptions, no-emoji workflow/
job `name:` (ADR-0005), kebab-case filenames.
- Zsh sources pass `zsh -n` and `zcompile`.
- Conventional Commits, PR-title validation, and the disallowed-trailer rule
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
name: CodeQL Advanced

on:
workflow_call:
inputs:
checkout-ref:
description: Optional exact candidate commit; otherwise check the triggering SHA.
required: false
type: string
push:
branches: [ "main" ]
pull_request:
Expand All @@ -13,7 +19,7 @@ permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-codeql-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -45,6 +51,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ inputs.checkout-ref || github.sha }}
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/commit-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ on:
required: false
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Required checks must not cancel another run for the same head when an edit
# and a push arrive together (z-shell/zi#597). These short jobs finish normally.

permissions:
contents: read
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/setup-zsh-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ on:
paths:
- "actions/setup-zsh/**"
- "automation/ci/test_setup_zsh.py"
- "automation/ci/test_zsh_ci.py"
- ".github/workflows/zsh-ci.yml"
- "automation/ci/verify_zsh_signals.py"
- ".github/workflows/setup-zsh-test.yml"
pull_request:
paths:
- "actions/setup-zsh/**"
- "automation/ci/test_setup_zsh.py"
- "automation/ci/test_zsh_ci.py"
- ".github/workflows/zsh-ci.yml"
- "automation/ci/verify_zsh_signals.py"
- ".github/workflows/setup-zsh-test.yml"
workflow_dispatch: {}
Expand Down Expand Up @@ -99,6 +103,9 @@ jobs:
autoload -Uz compinit
compinit -D
' zsh "$EXPECTED_VERSION" "$SOURCE_EXECUTABLE"
- name: Test native workflow inventory and failures
if: matrix.os == 'ubuntu-latest' && matrix.version == 'latest'
run: python3 -m unittest automation/ci/test_zsh_ci.py -v
- name: Verify explicit patch profile
if: matrix.patch != 'none'
env:
Expand Down
63 changes: 48 additions & 15 deletions .github/workflows/zsh-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,36 @@ name: Zsh CI
on:
workflow_call:
inputs:
checkout-ref:
description: Optional exact candidate commit; otherwise check the triggering SHA.
required: false
type: string
default: ""
extra-files:
description: Newline-delimited repository-relative Zsh files in addition to *.zsh (for example extensionless completions). No globs or symlinks.
required: false
type: string
default: ""
require-sources:
description: Fail when the selected source inventory is empty.
required: false
type: boolean
default: false
zsh-version:
description: Zsh version to install (latest package, or exact Linux 5.9 or 5.9.2).
required: false
type: string
default: latest
run-zcompile:
description: Run zcompile on all .zsh files in addition to zsh -n.
description: Run zcompile on all selected sources in addition to zsh -n.
required: false
type: boolean
default: false

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
group: ${{ github.workflow }}-native-zsh-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand All @@ -30,6 +45,9 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ inputs.checkout-ref || github.sha }}
persist-credentials: false

- name: Set up Zsh
uses: z-shell/.github/actions/setup-zsh@6c7972d4f682c29cdd0e86df2cffce6aa5d23c89 # main
Expand All @@ -38,29 +56,44 @@ jobs:

- name: Run zsh -n
shell: bash
env:
EXTRA_FILES: ${{ inputs.extra-files }}
REQUIRE_SOURCES: ${{ inputs.require-sources }}
RUN_ZCOMPILE: ${{ inputs.run-zcompile }}
run: |
set -euo pipefail
mapfile -d '' files < <(find . -type d -name '.git' -prune -o -type f -name '*.zsh' -print0)
while IFS= read -r file || [[ -n $file ]]; do
[[ -n $file ]] || continue
if [[ $file == /* || /$file/ == */../* || ! -f $file || -L $file ]]; then
echo '::error::Extra Zsh sources must be existing repository-relative regular files'
exit 1
fi
# Reject symlink ancestors as well as a symlink at the leaf.
parent=$(dirname -- "$file")
while [[ $parent != . ]]; do
[[ ! -L $parent ]] || { echo '::error::Symlink source directory'; exit 1; }
parent=$(dirname -- "$parent")
done
files+=("$file")
done <<< "$EXTRA_FILES"
if [[ ${#files[@]} -eq 0 ]]; then
echo "No .zsh files found — nothing to check."
if [[ $REQUIRE_SOURCES == true ]]; then
echo '::error::No Zsh sources selected'
exit 1
fi
echo 'No Zsh sources selected.'
exit 0
fi
compiled=$(mktemp -d)
trap 'rm -rf -- "$compiled"' EXIT
failed=0
for f in "${files[@]}"; do
if ! zsh -n "$f" 2>&1; then
if ! zsh -f -n -- "$f" 2>&1; then
echo "::error file=$f::zsh -n syntax check failed"
failed=1
fi
done
exit "$failed"

- name: Run zcompile
if: inputs.run-zcompile
shell: bash
run: |
mapfile -d '' files < <(find . -type d -name '.git' -prune -o -type f -name '*.zsh' -print0)
failed=0
for f in "${files[@]}"; do
if ! zsh -fc 'zcompile -- "$1"' zsh "$f" 2>&1; then
if [[ $RUN_ZCOMPILE == true ]] && ! zsh -fc 'zcompile "$1" "$2"' zsh "$compiled/source.zwc" "$f" 2>&1; then
echo "::error file=$f::zcompile failed"
failed=1
fi
Expand Down
8 changes: 7 additions & 1 deletion automation/ci/test-commit-lint-policy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ check_fallbacks_present() {
# input is a second source of truth that the pull_request path never reads.
check_no_input_defaults() {
checks=$((checks + 1))
if sed -n '/workflow_call:/,/^concurrency:/p' "$WORKFLOW" | grep -q '^ *default:'; then
if sed -n '/workflow_call:/,/^permissions:/p' "$WORKFLOW" | grep -q '^ *default:'; then
fail "workflow_call input carries a default that a pull_request run ignores (#586)"
fi
}
Expand Down Expand Up @@ -280,6 +280,12 @@ check_conventional_cases
check_issue_reference_cases
check_empty_pattern_is_never_harmless

# Metadata edits and pushes for one head must not cancel required results.
checks=$((checks + 1))
if grep -q '^concurrency:' "$WORKFLOW"; then
fail 'commit lint must let overlapping required-check runs finish (zi#597)'
fi

if [ "$failures" -gt 0 ]; then
printf '\n%d of %d checks failed\n' "$failures" "$checks" >&2
exit 1
Expand Down
106 changes: 106 additions & 0 deletions automation/ci/test_zsh_ci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Execute the native workflow's shell block against isolated source trees."""

import os
import shutil
import subprocess
import tempfile
import textwrap
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = ROOT / ".github/workflows/zsh-ci.yml"


class NativeWorkflowTests(unittest.TestCase):
def run_check(
self,
files,
extra="",
required="true",
compile="true",
links=None,
fail_compile=False,
):
source = WORKFLOW.read_text().split(" - name: Run zsh -n\n", 1)[1]
script = textwrap.dedent(source.split(" run: |\n", 1)[1])
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name, content in files.items():
target = root / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content)
for name, target in (links or {}).items():
(root / name).symlink_to(root / target)
env = dict(
os.environ,
EXTRA_FILES=extra,
REQUIRE_SOURCES=required,
RUN_ZCOMPILE=compile,
)
if fail_compile:
binary = root / "bin"
binary.mkdir()
stub = binary / "zsh"
stub.write_text(
'#!/bin/sh\n[ "$1" != -fc ] || exit 42\n'
f'exec "{shutil.which("zsh")}" "$@"\n'
)
stub.chmod(0o700)
env["PATH"] = str(binary) + os.pathsep + env["PATH"]
result = subprocess.run(
["bash", "-c", script],
cwd=root,
text=True,
capture_output=True,
env=env,
)
self.assertFalse(list(root.rglob("*.zwc")))
return result

def test_extensionless_and_space_paths(self):
result = self.run_check(
{"lib/_zi": "print ok\n", "with space.zsh": "print ok\n"}, "lib/_zi"
)
self.assertEqual(result.returncode, 0, result.stderr)

def test_invalid_extensionless_source_fails(self):
result = self.run_check({"lib/_zi": "if then\n"}, "lib/_zi")
self.assertNotEqual(result.returncode, 0)

def test_empty_inventory_contract(self):
self.assertNotEqual(self.run_check({}).returncode, 0)
self.assertEqual(self.run_check({}, required="false").returncode, 0)

def test_invalid_extra_paths_fail(self):
for path in ("../outside", "/etc/passwd", "missing", "*.zsh"):
with self.subTest(path=path):
self.assertNotEqual(
self.run_check({"ok.zsh": "true\n"}, path).returncode, 0
)

def test_symlink_sources_fail(self):
files = {"lib/_zi": "true\n"}
for extra, links in (
("link", {"link": "lib/_zi"}),
("link/_zi", {"link": "lib"}),
):
with self.subTest(extra=extra):
self.assertNotEqual(
self.run_check(files, extra, links=links).returncode, 0
)

def test_compiler_failure_propagates_only_when_enabled(self):
for enabled, expected in (("true", False), ("false", True)):
result = self.run_check(
{"ok.zsh": "true\n"}, compile=enabled, fail_compile=True
)
self.assertEqual(result.returncode == 0, expected)

def test_syntax_failure_cannot_be_hidden_by_later_success(self):
result = self.run_check({"bad.zsh": "if then\n", "good.zsh": "true\n"})
self.assertNotEqual(result.returncode, 0)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions knowledge/domains/ci/repository-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Select a file for the task described below. Imported files have an editable know
| [automation/ci/test_run_zd.py](../../../automation/ci/test_run_zd.py) | referenced | CI maintenance programs, adjacent tests and offline fixtures; preserve workflow inputs, runtime targets and sparse-checkout contracts. |
| [automation/ci/test_setup_zsh.py](../../../automation/ci/test_setup_zsh.py) | referenced | CI maintenance programs, adjacent tests and offline fixtures; preserve workflow inputs, runtime targets and sparse-checkout contracts. |
| [automation/ci/test_validate_zsh_standard_policy.py](../../../automation/ci/test_validate_zsh_standard_policy.py) | referenced | CI maintenance programs, adjacent tests and offline fixtures; preserve workflow inputs, runtime targets and sparse-checkout contracts. |
| [automation/ci/test_zsh_ci.py](../../../automation/ci/test_zsh_ci.py) | referenced | Executes the shared native Zsh workflow against isolated valid, invalid and extensionless sources. |
| [automation/ci/test_zsh_lint_ci.py](../../../automation/ci/test_zsh_lint_ci.py) | referenced | CI maintenance programs, adjacent tests and offline fixtures; preserve workflow inputs, runtime targets and sparse-checkout contracts. |
| [automation/ci/trunk-safe-check.sh](../../../automation/ci/trunk-safe-check.sh) | referenced | CI maintenance programs, adjacent tests and offline fixtures; preserve workflow inputs, runtime targets and sparse-checkout contracts. |
| [automation/ci/trunk-safe-ci.sh](../../../automation/ci/trunk-safe-ci.sh) | referenced | CI maintenance programs, adjacent tests and offline fixtures; preserve workflow inputs, runtime targets and sparse-checkout contracts. |
Expand Down
16 changes: 15 additions & 1 deletion knowledge/domains/ci/workflow-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ This is an interim compatibility measure for [#663](https://github.com/z-shell/.
## 3. Concurrency & Execution Control

- **Branch / PR Workflows**: Declare a `concurrency` block with `cancel-in-progress: true` to prevent resource waste and race conditions on rapid pushes.
- **Required checks with overlapping events**: When push and PR metadata events can produce checks for the same head, do not cancel or supersede those short runs. GitHub retains cancelled check results; even `cancel-in-progress: false` can replace a pending run. Omit concurrency for this documented case (z-shell/zi#597), and verify the effective required checks. A reusable workflow that owns concurrency must use a group distinct from its caller, or the caller must omit its own group.
- **Release / Deployment Workflows**: Set `cancel-in-progress: false` to ensure in-flight deployments complete deterministically.

---
Expand All @@ -85,6 +86,19 @@ This is an interim compatibility measure for [#663](https://github.com/z-shell/.
- Reference called workflows using pinned immutable refs.
- Expose job `outputs` cleanly for downstream dependent jobs (`needs:`).

The shared `zsh-ci.yml` owns native syntax and optional compilation. Callers
declare extensionless Zsh sources through newline-delimited `extra-files`,
set `require-sources: true` when an empty inventory is an error, and enable
`run-zcompile` for the organization baseline. Existing `*.zsh` discovery is
retained. Compilation uses disposable output outside the checkout. Consumers
own source classification, supported versions and functional tests.

The shared `codeql.yml` supports `workflow_call` for Actions
analysis; it does not analyze Zsh semantics. Both native Zsh and CodeQL accept
`checkout-ref` for an exact candidate SHA and use distinct concurrency groups
when called from the same parent workflow. Preserve required-check names and
prove caller coverage before changing repository rulesets.

---

## 5. Organization-Retired Patterns
Expand All @@ -106,7 +120,7 @@ new uses; migrate existing uses through their owning rollout and runbook.
- [ ] Filename is `kebab-case.yml` with appropriate category prefix.
- [ ] Workflow `name:` and Job `name:` contain NO emojis.
- [ ] Top-level `permissions:` is declared with minimum necessary scope.
- [ ] `concurrency:` block is present with `cancel-in-progress` set appropriately.
- [ ] Concurrency follows the execution-control rules above, including documented required-check exceptions and distinct caller/callee groups.
- [ ] All remote actions and reusable workflows are pinned to 40-character commit SHAs with associated release comments, or the documented interim `# main` comment for organization reusable workflows.
- [ ] Any interim pinact exception uses the shared template's exact repository/workflow and full-SHA match; the selected workflow and commit are verified separately.
- [ ] Actionlint and YAML syntax checks pass.
Loading
Loading