Skip to content

Commit 5520ade

Browse files
committed
feat(lint): add shell script linting action and integrate into workflows
1 parent e75430c commit 5520ade

File tree

7 files changed

+220
-10
lines changed

7 files changed

+220
-10
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: "Check shell scripts"
2+
description: "Check shell scripts"
3+
runs:
4+
using: "composite"
5+
steps:
6+
- name: "Check shell scripts"
7+
shell: bash
8+
run: |
9+
export BRANCH_NAME=origin/${{ github.event.repository.default_branch }}
10+
make check-shell-lint

.github/workflows/stage-1-commit.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,14 @@ jobs:
5757
fetch-depth: 0 # Full history is needed to compare branches
5858
- name: "Check Markdown links"
5959
uses: ./.github/actions/check-markdown-links
60+
check-shell-lint:
61+
name: "Check shell scripts"
62+
runs-on: ubuntu-latest
63+
timeout-minutes: 2
64+
steps:
65+
- name: "Checkout code"
66+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
67+
with:
68+
fetch-depth: 0 # Full history is needed to compare branches
69+
- name: "Check shell scripts"
70+
uses: ./.github/actions/check-shell-lint

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ lint-markdown-format: # Check markdown formatting @Quality
1515
lint-markdown-links: # Check markdown links @Quality
1616
output=$$(check=all ./scripts/quality/check-markdown-links.sh 2>&1) && echo "markdown links: ok" || { echo "$$output"; exit 1; }
1717

18+
lint-shell: # Check shell scripts @Quality
19+
$(MAKE) check-shell-lint
20+
1821
lint: # Run linter to check code style and errors @Quality
1922
$(MAKE) lint-file-format
2023
$(MAKE) lint-markdown-format
2124
$(MAKE) lint-markdown-links
25+
$(MAKE) lint-shell
2226

2327
test: # Run all tests @Testing
2428
# No tests required for this repository
@@ -78,6 +82,7 @@ ${VERBOSE}.SILENT: \
7882
lint-file-format \
7983
lint-markdown-format \
8084
lint-markdown-links \
85+
lint-shell \
8186
patch-speckit \
8287
specify \
8388
test \

scripts/apply.sh

Lines changed: 175 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ function main() {
194194
fi
195195

196196
# Auto-enable rust, typescript, and reactjs if tauri is specified
197+
# shellcheck disable=SC2034
197198
if is-arg-true "${tauri:-false}"; then
198199
rust=true
199200
typescript=true
@@ -255,6 +256,7 @@ function main() {
255256
copy-docs-prompts "${destination}"
256257
copy-workspace-file "${destination}"
257258
update-gitignore "${destination}"
259+
update-vscode-settings "${destination}"
258260

259261
echo
260262
echo "Done. Assets copied to ${destination}"
@@ -318,7 +320,7 @@ function wipe-directories() {
318320
for dir in "${dirs[@]}"; do
319321
if [[ -d "${dest}/${dir}" ]]; then
320322
print-info "Removing ${dest}/${dir}"
321-
rm -rf "${dest}/${dir}"
323+
rm -rf "${dest:?}/${dir}"
322324
fi
323325
done
324326
}
@@ -585,8 +587,6 @@ function copy-docs-prompts() {
585587

586588
print-info "Copying docs/prompts to ${dest}/prompts"
587589
cp -R "${DOCS_PROMPTS}/." "${dest}/prompts/"
588-
print-info "Creating docs/.gitignore"
589-
echo "prompts" > "${dest}/.gitignore"
590590
}
591591

592592
# Copy project.code-workspace to the destination if it does not already exist.
@@ -660,6 +660,178 @@ function update-gitignore() {
660660
return 0
661661
}
662662

663+
# Update .vscode/settings.json with promptfiles settings.
664+
# Arguments (provided as function parameters):
665+
# $1=[destination directory path]
666+
function update-vscode-settings() {
667+
668+
local dest="$1"
669+
local settings_dir="${dest}/.vscode"
670+
local settings_file="${settings_dir}/settings.json"
671+
672+
mkdir -p "${settings_dir}"
673+
674+
if [[ ! -f "${settings_file}" || ! -s "${settings_file}" ]]; then
675+
print-info "Creating VS Code settings: ${settings_file}"
676+
cat <<'EOF' > "${settings_file}"
677+
{
678+
"chat.promptFilesRecommendations": {
679+
"speckit.constitution": true,
680+
"speckit.specify": true,
681+
"speckit.plan": true,
682+
"speckit.tasks": true,
683+
"speckit.implement": true
684+
},
685+
"chat.tools.terminal.autoApprove": {
686+
".specify/scripts/bash/": true
687+
}
688+
}
689+
EOF
690+
return 0
691+
fi
692+
693+
# Always remove existing sections before adding them back
694+
remove-vscode-json-property "${settings_file}" "chat.promptFilesRecommendations"
695+
remove-vscode-json-property "${settings_file}" "chat.tools.terminal.autoApprove"
696+
697+
# Prepare content to insert (always both sections)
698+
local insert_file
699+
insert_file=$(mktemp)
700+
701+
cat <<'EOF' >> "${insert_file}"
702+
"chat.promptFilesRecommendations": {
703+
"speckit.constitution": true,
704+
"speckit.specify": true,
705+
"speckit.plan": true,
706+
"speckit.tasks": true,
707+
"speckit.implement": true
708+
},
709+
"chat.tools.terminal.autoApprove": {
710+
".specify/scripts/bash/": true
711+
}
712+
EOF
713+
714+
local last_brace_line
715+
last_brace_line=$(awk '/}/ { line=NR } END { print line }' "${settings_file}")
716+
717+
if [[ -z "${last_brace_line}" ]]; then
718+
rm -f "${insert_file}"
719+
print-error "Invalid ${settings_file}: missing closing brace"
720+
fi
721+
722+
local prev_line_num
723+
prev_line_num=$(awk -v last="${last_brace_line}" 'NR < last { if ($0 ~ /[^[:space:]]/) { line=NR } } END { print line }' "${settings_file}")
724+
725+
local needs_comma=0
726+
if [[ -n "${prev_line_num}" ]]; then
727+
local prev_line
728+
prev_line=$(sed -n "${prev_line_num}p" "${settings_file}")
729+
if [[ "${prev_line}" =~ ^[[:space:]]*\{[[:space:]]*$ ]]; then
730+
needs_comma=0
731+
elif [[ "${prev_line}" =~ ,[[:space:]]*$ ]]; then
732+
needs_comma=0
733+
else
734+
needs_comma=1
735+
fi
736+
fi
737+
738+
local temp_file
739+
temp_file=$(mktemp)
740+
741+
awk -v insert_file="${insert_file}" -v insert_line="${last_brace_line}" -v prev_line="${prev_line_num}" -v needs_comma="${needs_comma}" '
742+
NR == prev_line && needs_comma == 1 {
743+
sub(/[[:space:]]*$/, "", $0)
744+
print $0 ","
745+
next
746+
}
747+
NR == insert_line {
748+
while ((getline line < insert_file) > 0) { print line }
749+
close(insert_file)
750+
print $0
751+
next
752+
}
753+
{ print }
754+
' "${settings_file}" > "${temp_file}"
755+
756+
mv "${temp_file}" "${settings_file}"
757+
rm -f "${insert_file}"
758+
759+
print-info "Updated VS Code settings: ${settings_file}"
760+
761+
return 0
762+
}
763+
764+
# Remove a JSON property from a VS Code settings file.
765+
# Arguments (provided as function parameters):
766+
# $1=[settings file path]
767+
# $2=[property name without quotes]
768+
function remove-vscode-json-property() {
769+
770+
local file="$1"
771+
local property="$2"
772+
773+
# If property doesn't exist, nothing to do
774+
if ! grep -q "\"${property}\"" "$file" 2>/dev/null; then
775+
return 0
776+
fi
777+
778+
local temp_file
779+
temp_file=$(mktemp)
780+
781+
awk -v prop="\"${property}\"" '
782+
BEGIN { skip = 0; depth = 0; skip_comma = 0 }
783+
{
784+
# Check if this line starts the property we want to remove
785+
if (!skip && match($0, prop "[[:space:]]*:[[:space:]]*\\{")) {
786+
skip = 1
787+
depth = 0
788+
# Count braces on the same line
789+
rest_of_line = substr($0, RSTART + RLENGTH)
790+
for (i = 1; i <= length(rest_of_line); i++) {
791+
c = substr(rest_of_line, i, 1)
792+
if (c == "{") depth++
793+
else if (c == "}") depth--
794+
}
795+
# If property closes on same line, stop skipping
796+
if (depth < 0) {
797+
skip = 0
798+
skip_comma = 1
799+
}
800+
next
801+
}
802+
803+
# While inside the property, track brace depth
804+
if (skip) {
805+
for (i = 1; i <= length($0); i++) {
806+
c = substr($0, i, 1)
807+
if (c == "{") depth++
808+
else if (c == "}") depth--
809+
}
810+
# When depth goes negative, we have closed the property
811+
if (depth < 0) {
812+
skip = 0
813+
skip_comma = 1
814+
}
815+
next
816+
}
817+
818+
# Skip a trailing comma line if needed
819+
if (skip_comma) {
820+
skip_comma = 0
821+
if ($0 ~ /^[[:space:]]*,[[:space:]]*$/) {
822+
next
823+
}
824+
}
825+
826+
print
827+
}
828+
' "$file" > "$temp_file"
829+
830+
mv "$temp_file" "$file"
831+
832+
return 0
833+
}
834+
663835
# Copy a directory without bringing across any nested .git metadata.
664836
# Arguments (provided as function parameters):
665837
# $1=[source directory path]

scripts/config/pre-commit.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,10 @@ repos:
2727
entry: make check-markdown-links check=all
2828
language: system
2929
pass_filenames: false
30+
- repo: local
31+
hooks:
32+
- id: check-shell-lint
33+
name: Check shell scripts
34+
entry: make check-shell-lint
35+
language: system
36+
pass_filenames: false

scripts/init.mk

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ check-markdown-links: check ?= all
1818
check-markdown-links: # Check markdown links (set check=all|staged-changes|working-tree-changes|branch) @Quality
1919
output=$$(check=all ./scripts/quality/check-markdown-links.sh 2>&1) && echo "markdown links: ok" || { echo "$$output"; exit 1; }
2020

21-
check-shell-lint: # Lint all shell scripts in this project, do not fail on error, just print the error messages @Quality
22-
output=$$(for file in $$(find . -type f -name "*.sh"); do
23-
file=$${file} scripts/quality/check-shell-lint.sh ||:;
24-
done 2>&1)
25-
if [ -z "$$output" ]; then
21+
check-shell-lint: # Lint all shell scripts in this project @Quality
22+
failed=0
23+
for file in $$(find . -type f -name "*.sh" \
24+
! -path "./.github/skills/repository-template/*" \
25+
! -path "./.specify/*"); do
26+
if ! file=$${file} scripts/quality/check-shell-lint.sh; then
27+
failed=1
28+
fi
29+
done
30+
if [ $$failed -eq 0 ]; then
2631
echo "shell lint: ok"
2732
else
28-
printf "%s\n" "$$output";
33+
exit 1
2934
fi
3035

3136
version-create-effective-file: # Create effective version file - optional: dir=[path to the VERSION file to use, default is '.'], BUILD_DATETIME=[build date and time in the '%Y-%m-%dT%H:%M:%S%z' format generated by the CI/CD pipeline, default is current date and time] @Development

scripts/patch-speckit.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function create-temp-directory() {
7979
# Clean up the temporary directory.
8080
# Arguments:
8181
# $1=[path to temporary directory]
82+
# shellcheck disable=SC2329
8283
function cleanup-temp-directory() {
8384
local temp_dir="$1"
8485
if [[ -d "$temp_dir" ]]; then
@@ -476,7 +477,6 @@ function extract-extension-body() {
476477
local content="$1"
477478
local result=""
478479
local footer_start_line=""
479-
local line_num=0
480480
local lines=()
481481

482482
# Read content into array

0 commit comments

Comments
 (0)