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
48 changes: 48 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
root = true

# Language/framework-agnostic base. Covers universal defaults plus the
# languages with a presence in this repo (Markdown, shell, PowerShell) and the
# generic Windows-native batch class. Language overlays (.NET, Python, …) extend
# this file with their own source sections when adopted; they do not ship a
# separate editorconfig.
#
# .gitattributes is the single authority for line endings (it transforms bytes
# on checkout); end_of_line here is an editor hint only, and editorconfig-checker
# leaves the end-of-line check disabled. See modules/editorconfig/.

[*]
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf

# Markdown — trailing whitespace is significant (two trailing spaces = a hard
# line break); indentation is variable per CommonMark.
[*.{md,markdown}]
trim_trailing_whitespace = false
indent_size = unset

# Config / serialization / data
[*.{json,jsonc,yml,yaml,toml}]
indent_size = 2

# Shell
[*.{sh,bash}]
indent_size = 2

# PowerShell — LF runs on both PowerShell 7 and Windows PowerShell 5.1 (verified
# empirically). See the .gitattributes note before changing this to crlf.
[*.{ps1,psm1,psd1}]
indent_size = 4

# Windows batch — cmd.exe requires CRLF.
[*.{cmd,bat}]
end_of_line = crlf

# Lockfiles — generated; suppress formatting enforcement.
[{package-lock.json,*.lock}]
indent_size = unset
insert_final_newline = unset
trim_trailing_whitespace = unset
60 changes: 60 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Language/framework-agnostic base. `* text=auto eol=lf` normalizes text to LF in
# the repository AND checks it out LF on every platform (deterministic, regardless
# of a developer's core.autocrlf). Windows-native types below override to CRLF. A
# later line overrides an earlier one, per attribute. This file is the single
# authority for line endings (editorconfig end_of_line is an editor hint only).

* text=auto eol=lf

###############################################################################
# Richer diffs (line endings inherit the LF default above).
###############################################################################

*.md diff=markdown
*.sh diff=bash
*.bash diff=bash

###############################################################################
# PowerShell — LF is deliberate. Verified to run on both PowerShell 7 and
# Windows PowerShell 5.1. The PowerShell repo pins .ps1 to eol=lf; the popular
# community gitattributes template pins crlf — they target different eras. Listed
# explicitly so it is not "corrected" to crlf without a requirement to round-trip
# pre-existing CRLF-signed scripts unchanged.
###############################################################################

*.ps1 text eol=lf
*.psm1 text eol=lf
*.psd1 text eol=lf

###############################################################################
# Windows-native — cmd.exe requires CRLF (overrides the LF default above).
###############################################################################

*.cmd text eol=crlf
*.bat text eol=crlf

###############################################################################
# Lockfiles — tracked, but suppress noisy diffs (regenerate to resolve).
###############################################################################

package-lock.json -diff
*.lock -diff

###############################################################################
# Binary — no line-ending conversion, no diff.
###############################################################################

*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.webp binary
*.pdf binary
*.zip binary
*.gz binary
*.7z binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
# Keep the actions and reusable workflows referenced by SHA current. Review
# every PR before merge; never auto-merge (a SHA bump is executable CI logic).
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: ci

on:
push:
pull_request:

permissions:
contents: read

# Dogfood: this repo is the first consumer of its own reusable workflows. Each
# lane runs against this repo's vendored config copy; the local ci-status
# gateway aggregates them into the single required check.
jobs:
markdown:
uses: ./.github/workflows/markdown.yml

powershell:
uses: ./.github/workflows/powershell.yml

ci-status:
if: always()
needs: [markdown, powershell]
runs-on: ubuntu-latest
steps:
- name: Aggregate lane results
env:
RESULTS: ${{ needs.markdown.result }} ${{ needs.powershell.result }}
run: |
for r in $RESULTS; do
case "$r" in
success|skipped) ;;
*) echo "A lane did not pass (result: $r)."; exit 1 ;;
esac
done
echo "All lanes passed or were skipped."
44 changes: 44 additions & 0 deletions .github/workflows/markdown.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: markdown

on:
workflow_call:
inputs:
globs:
description: >-
markdownlint-cli2 glob arguments, space-separated (word-split on the
runner). Prefix a glob with '!' to exclude. Default lints all markdown.
type: string
default: '**/*.md'
config:
description: Path to the markdownlint-cli2 config file in the caller repo.
type: string
default: modules/markdown/.markdownlint-cli2.jsonc
version:
description: Exact markdownlint-cli2 version to run.
type: string
default: 0.22.1
node-version:
description: Node.js version to set up.
type: string
default: 24.17.0

permissions:
contents: read

jobs:
markdownlint:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ inputs.node-version }}
- name: Lint markdown
env:
CONFIG: ${{ inputs.config }}
VERSION: ${{ inputs.version }}
GLOBS: ${{ inputs.globs }}
# $GLOBS is intentionally unquoted so multiple glob arguments word-split.
run: npx --yes "markdownlint-cli2@$VERSION" --config "$CONFIG" $GLOBS

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent Bash from pre-expanding markdown globs

When a caller uses the default **/*.md in a repo with a match like docs/guide.md, this unquoted expansion is performed by the GitHub runner's Bash before markdownlint-cli2 sees it; with Bash globstar off by default, the shell passes only its one-directory matches and omits root/deeper files such as README.md/a/b/file.md. markdownlint-cli2's own usage notes recommend quoting glob arguments because shells expand globs differently, so the advertised default no longer lints all Markdown in common layouts.

Useful? React with 👍 / 👎.

80 changes: 80 additions & 0 deletions .github/workflows/powershell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: powershell

on:
workflow_call:
inputs:
paths:
description: >-
Files/dirs to analyze (relative to the caller repo), space-separated.
Directories recurse. Default analyzes the whole repo.
type: string
default: '.'
settings:
description: Path to the PSScriptAnalyzerSettings.psd1 ruleset in the caller repo.
type: string
default: modules/powershell/PSScriptAnalyzerSettings.psd1
exclude:
description: >-
Path substrings to skip, space-separated (e.g. intentionally-bad
fixtures). The .git directory is always skipped.
type: string
default: ''
analyzer-version:
description: Exact PSScriptAnalyzer version to install and require.
type: string
default: 1.25.0

permissions:
contents: read

jobs:
pssa:
runs-on: ubuntu-latest
steps:
- name: Check out caller
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: caller

# The runner script lives in this repo, not the caller's; fetch it via the
# job context (PAT-free — this repo is already authorized for the run).
- name: Check out runner script
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ job.workflow_repository }}
ref: ${{ job.workflow_sha }}
Comment on lines +44 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Checkout the reusable workflow repository

In cross-repo consumers of this reusable workflow, these expressions are empty because the documented contexts expose github.workflow_sha/job.workflow_ref, not job.workflow_repository or job.workflow_sha (GitHub docs); GitHub also evaluates missing properties to an empty string. That means the second checkout does not fetch the ci-workflows commit that contains scripts/Invoke-Pssa.ps1 (it falls back to the triggering repo/ref or otherwise has blank inputs), so the later Analyze step cannot run the runner script unless the caller happened to copy it.

Useful? React with 👍 / 👎.

path: runner

- name: Ensure PSScriptAnalyzer
shell: pwsh
env:
REQUIRED: ${{ inputs.analyzer-version }}
run: |
$required = $env:REQUIRED
$have = Get-Module -ListAvailable PSScriptAnalyzer |
Where-Object { $_.Version -eq [version]$required }
if (-not $have) {
try {
Install-Module PSScriptAnalyzer -RequiredVersion $required -Scope CurrentUser -Force -SkipPublisherCheck -ErrorAction Stop
} catch {
Install-PSResource PSScriptAnalyzer -Version $required -Scope CurrentUser -TrustRepository -Reinstall -ErrorAction Stop
}
}
Import-Module PSScriptAnalyzer -RequiredVersion $required -ErrorAction Stop
Write-Output "PSScriptAnalyzer $((Get-Module PSScriptAnalyzer).Version) on pwsh $($PSVersionTable.PSVersion)"

- name: Analyze
working-directory: caller
shell: pwsh
env:
PATHS: ${{ inputs.paths }}
SETTINGS: ${{ inputs.settings }}
EXCLUDE: ${{ inputs.exclude }}
REQUIRED: ${{ inputs.analyzer-version }}
run: |
# Wrap in @() so an empty input stays an empty array, never $null
# (a bare null would violate the runner's non-null parameter contract).
$paths = @($env:PATHS -split '\s+' | Where-Object { $_ })
$exclude = @($env:EXCLUDE -split '\s+' | Where-Object { $_ })
../runner/scripts/Invoke-Pssa.ps1 -Path $paths -Settings $env:SETTINGS -AnalyzerVersion $env:REQUIRED -ExcludePath $exclude
exit $LASTEXITCODE
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# OS / editor noise
.DS_Store
Thumbs.db
*.swp
.idea/
.vs/
.vscode/*
!.vscode/extensions.json

# Claude Code worktrees (per-checkout; PR worktree workflow)
.claude/worktrees/

# Node
node_modules/
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,44 @@
# ci-workflows
Reusable, configurable CI workflows (referenced via uses:@sha). Consumers feed a local ci-status gate.

Reusable, configurable CI **execution** for the melodic-software org: the
workflows that install and run each code-quality tool, plus the runner scripts
they call.

Consumed by reference, never copied:

```yaml
jobs:
markdown:
uses: melodic-software/ci-workflows/.github/workflows/markdown.yml@<sha>
```

## Contract

- **Configurable, not forkable.** Each reusable workflow exposes typed `inputs`
with global-standard defaults. Consumers override repo-specific scope (globs,
paths, tool versions, config location) through inputs — never by editing the
workflow.
- **Pin by SHA.** Reference every reusable workflow at a full commit SHA;
Dependabot (`github-actions`, weekly) opens bump PRs that are reviewed and
merged manually.
- **Each consumer aggregates locally.** A reusable workflow runs one tool. The
required-check contract is a single check named `ci-status`, produced by a
thin gateway job the consumer keeps local so the required-check name stays
un-nested. The gateway `needs:` the called jobs and fails if any failed or was
cancelled; a skipped job is allowed.

## Workflows

- `markdown.yml` — markdownlint-cli2 over the repo's markdown.
- `powershell.yml` — PSScriptAnalyzer over the repo's PowerShell, via
`scripts/Invoke-Pssa.ps1` (per-file subprocess isolation).

Each input's meaning and default is documented inline in the workflow's
`inputs:` block.

## Tool configuration lives elsewhere

These workflows execute tools; they do not carry the tools' rulesets. A consumer
supplies its own config file and points the workflow at it through an input, so
adopting a workflow never couples the consumer to this repo at runtime beyond
the referenced workflow itself.
37 changes: 37 additions & 0 deletions modules/markdown/.markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
// GitHub Flavored Markdown (GFM) ruleset for markdownlint-cli2.
// Schema pinned to markdownlint-cli2 v0.22.1 — bump together on upgrade.
// Rules reference: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
// All rules are enabled by default; only deviations are listed below.
// Globs are passed by the caller (CLI / CI), not declared here.
"$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.22.1/schema/markdownlint-cli2-config-schema.json",
"ignores": [
// Build, dependency, and cache trees — not authored markdown.
"**/node_modules/**",
"**/.venv/**",
"**/bin/**",
"**/obj/**"
],
"config": {
// --- GFM-aligned style ---
"MD003": { "style": "atx" }, // ATX headings (# Heading)
"MD004": { "style": "dash" }, // Dash unordered-list bullets
"MD049": { "style": "asterisk" }, // *emphasis*
"MD050": { "style": "asterisk" }, // **strong**
"MD046": { "style": "fenced" }, // Fenced (not indented) code blocks
"MD048": { "style": "backtick" }, // Backtick (not tilde) code fences
"MD024": { "siblings_only": true }, // Allow duplicate headings under different parents
"MD055": { "style": "consistent" }, // Consistent table pipe style within a file
"MD060": false, // Don't enforce one table-column style across mixed compact/padded tables

// --- Relaxed for GFM / prose ---
"MD013": false, // No hard line-length limit (tables and code exceed 80)
"MD025": false, // Allow multiple top-level (H1) sections
"MD028": false, // Allow blank lines between adjacent blockquotes
"MD033": false, // Allow inline HTML (<details>, <summary>, <br>, ...)
"MD034": false, // Allow bare URLs (GFM autolinks them)
"MD036": false, // Allow bold text used as a pseudo-heading
"MD040": false, // Fenced code need not declare a language
"MD041": false // First line need not be a top-level heading (frontmatter)
}
}
Loading