Repository navigation
Expand file tree
/
Copy pathTaskfile.yml
More file actions
233 lines (211 loc) · 13.6 KB
/
Copy pathTaskfile.yml
File metadata and controls
233 lines (211 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
version: '3'
# Host-side settings (DEVCONTAINER_DOCKER_PATH, ...) may live in a .env file -
# see bootstrap/env.example.
dotenv: ['.env', '.devcontainer/.env']
includes:
devcontainer:
taskfile: ./bootstrap/devcontainer.yml
aliases: [dc] # task dc:up == task devcontainer:up, as in the Taskfile the bootstrap writes
# Tasks contributed by the Features installed in this repo's devcontainer
# (go-task:*, devcontainer-cli:*); absent on the host.
features:
taskfile: /usr/local/share/go-task/features.yml
optional: true
flatten: true
vars:
TEST_IMAGE: '{{.IMAGE | default "mcr.microsoft.com/devcontainers/base:debian"}}'
tasks:
default:
silent: true
cmds:
- task --list
test:
desc: 'Run `devcontainer features test` for one Feature (FEATURE=go-task) or all of them. IMAGE overrides the base image for the auto-generated test; SCENARIOS=false skips scenarios.json. Needs a Docker daemon (CI, or Docker Desktop on the workstation). Linux/macOS only (the CLI shells out to chmod/bash on the host).'
platforms: [linux, darwin]
vars:
FEATURE: '{{.FEATURE | default ""}}'
SCENARIOS: '{{.SCENARIOS | default "true"}}'
preconditions:
- sh: docker info >/dev/null 2>&1
msg: 'no Docker daemon here; Feature tests run in CI on every PR, or locally with Docker Desktop on the workstation'
cmds:
- task: devcontainer:cli
vars:
SUBCOMMAND: features
ARGS: 'test -p "{{.ROOT_DIR}}" -i {{.TEST_IMAGE}} --skip-duplicated{{if .FEATURE}} -f {{.FEATURE}}{{end}}{{if ne .SCENARIOS "true"}} --skip-scenarios{{end}} {{.CLI_ARGS}}'
test:global:
desc: 'Run only test/_global scenarios (Features combined). Needs a Docker daemon (CI, or Docker Desktop on the workstation). Linux/macOS only, like test.'
platforms: [linux, darwin]
preconditions:
- sh: docker info >/dev/null 2>&1
msg: 'no Docker daemon here; Feature tests run in CI on every PR, or locally with Docker Desktop on the workstation'
cmds:
- task: devcontainer:cli
vars:
SUBCOMMAND: features
ARGS: 'test -p "{{.ROOT_DIR}}" --global-scenarios-only'
docs:
desc: 'Regenerate src/<id>/README.md from each devcontainer-feature.json + NOTES.md with `devcontainer features generate-docs`. Run after changing either and commit the result; CI (lint) fails when a README is stale.'
# Relative -p on purpose: the CLI embeds the given path in each README's
# footer link, so an absolute path would leak the local checkout path.
dir: '{{.ROOT_DIR}}'
cmds:
- task: devcontainer:cli
vars:
SUBCOMMAND: features
ARGS: 'generate-docs -p src -n nabil-becker/devcontainer-features --github-owner nabil-becker --github-repo devcontainer-features'
docs:check:
desc: 'Fail if any src/<id>/README.md in the working tree differs from what `task docs` generates (the check CI runs). Compares before/after regeneration, so it works on uncommitted changes too.'
dir: '{{.ROOT_DIR}}'
cmds:
- cmd: |
snapshot() { for f in src/*/README.md; do [ -f "$f" ] && printf '%s ' "$f" && sha256sum < "$f"; done; }
before="$(snapshot)"
task docs >/dev/null
after="$(snapshot)"
if [ "$before" != "$after" ]; then
echo "src/<id>/README.md was stale - regenerated now; review and commit:"; git status --short -- 'src/*/README.md'; exit 1
fi
echo "generated READMEs are up to date"
lint:
desc: 'shellcheck every shell script, yamllint every YAML file (.yamllint), PSScriptAnalyzer every PowerShell file, the vendored copies of src/common/*.sh (in src/ and local-features/) identical to the canonical ones, and the Docker-free unit tests (devhost tooling, the git Feature''s .git-dirs mapping) (same checks as CI; the tools must be installed - this repo''s devcontainer has them).'
cmds:
- shellcheck -x src/*/*.sh local-features/*/*.sh local-features/*/share/bin/*.sh local-features/*/share/jobs/*/*.sh test/*/*.sh test/*/unit/*.sh test/bootstrap/*.sh bootstrap/*.sh bootstrap/host/*.sh .githooks/*
- yamllint --strict .
- pwsh -NoLogo -NoProfile -File '{{.ROOT_DIR}}/.github/scripts/Invoke-PSScriptAnalyzer.ps1'
- task: common:check
- task: taskfiles:check
- task: test:devhost
- task: test:git-unit
- task: test:bootstrap
- task: test:shutdown
- task: test:host-type
- task: test:bootstrap-dir
test:git-unit:
desc: 'Docker-free tests of the git Feature''s wrapper, git-setup and .git-dirs mapping, run from src/git against temp folders (never the checkout''s own git dir or global config); part of lint.'
cmds:
- bash test/git/unit/git_dirs_test.sh
test:shutdown:
desc: 'Docker-free bash tests of the shutdown-check contract (bootstrap/SHUTDOWN.md): the default resolution snippet from bootstrap/devcontainer.yml against fake task and devhost-shutdown-check commands, the starter bootstrap/shutdown-check.sh in temp git repos, and the bash twin''s stop (127 message, refusal, FORCE, prepshutdown) with a stub engine and CLI; part of lint.'
cmds:
- bash test/bootstrap/shutdown_resolution_test.sh
test:host-type:
desc: 'Docker-free bash tests of HOST_TYPE resolution in the bash twin (src/devcontainer-cli/devcontainer-cli.sh): explicit id, unknown id, auto and HOST_TYPE_PRIORITY (default and an organisation list), the DCF_HOST_* variables handed to the CLI, DEVCONTAINER_DOCKER_PATH still naming the engine; evidence faked with DCF_HOST_UNAME, DCF_HOST_PROC_VERSION and fake docker and devcontainer commands; part of lint. The PowerShell twin is covered by test:bootstrap.'
cmds:
- bash test/bootstrap/host_type_test.sh
test:bootstrap-dir:
desc: 'Docker-free, offline bash tests of where bootstrap/bootstrap.sh vendors: the default .devcontainer/cli_bootstrap.ignore, BOOTSTRAP_DIR and its rejections (absolute, ..), the .gitignore created, appended once, other lines kept, BOOTSTRAP_GITIGNORE=false, the Taskfile template''s optional include of the chosen path, and the old ./bootstrap form (kept, migration note, include repointed); against a temp repo with this checkout as --source and a fake task; part of lint. The PowerShell twin is in test:bootstrap.'
cmds:
- bash test/bootstrap/bootstrap_dir_test.sh
test:bootstrap:
desc: 'Pester tests of the Windows host launcher (bootstrap/host/DevcontainerCli.psm1) and of bootstrap/bootstrap.ps1 (vendoring folder, BOOTSTRAP_DIR, git-ignore, Taskfile template, old form): the build lock that serialises devcontainer up/build across repositories, the stop check, and HOST_TYPE resolution and the DCF_HOST_* variables (evidence mocked). Needs pwsh with Pester 5 or later (Install-Module Pester -Scope CurrentUser); runs on any host, so it is part of lint.'
cmds:
- pwsh -NoLogo -NoProfile -Command "Import-Module Pester -MinimumVersion 5.0; Invoke-Pester -Path '{{.ROOT_DIR}}/test/bootstrap' -Output Detailed -CI"
test:devhost:
desc: 'Unit tests of the devhost Feature''s (local-features/devhost) Python tooling (session cost, guards, verify, adr, init, briefing), stdlib only, against temp repos; runs on any host with python3 and git, so it is part of lint.'
cmds:
- cmd: python -m unittest discover -s test/devhost/python -q
platforms: [windows]
- cmd: python3 -m unittest discover -s test/devhost/python -q
platforms: [linux, darwin]
test:devhost:docker:
desc: 'Run the devhost Feature''s Docker tests (test/devhost: defaults and the non_root scenario) for local-features/devhost, which is not under src/ and so is outside `task test`, CI and the global scenarios. Copies it and its tests into a temporary project folder shaped like a collection (src/devhost, test/devhost) and runs `devcontainer features test` there. IMAGE overrides the base image. Needs a Docker daemon and the devcontainer CLI. Linux/macOS only.'
platforms: [linux, darwin]
preconditions:
- sh: docker info >/dev/null 2>&1
msg: 'no Docker daemon here; the devhost Docker tests run only where Docker does'
cmds:
- cmd: |
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/src" "$tmp/test"
cp -r local-features/devhost "$tmp/src/devhost"
cp -r test/devhost "$tmp/test/devhost"
devcontainer features test -p "$tmp" -i {{.TEST_IMAGE}} -f devhost {{.CLI_ARGS}}
features:stage:
desc: 'Copy every src/<id> Feature and every local-features/<id> Feature (used here, never published) into .devcontainer/features/<id> (gitignored), so this repo''s own devcontainer references the unpublished Features by local path. The devcontainer runs it as initializeCommand before every start.'
silent: true
cmds:
- cmd: pwsh -NoLogo -NoProfile -Command 'Remove-Item -Recurse -Force .devcontainer/features -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Force .devcontainer/features | Out-Null; $d = @(Get-ChildItem src, local-features -Directory -ErrorAction SilentlyContinue); $d | ForEach-Object { Copy-Item -Recurse $_.FullName (Join-Path .devcontainer/features $_.Name) }; "staged $($d.Count) Features into .devcontainer/features"'
platforms: [windows]
- cmd: rm -rf .devcontainer/features && mkdir -p .devcontainer/features && n=0; for d in src/*/ local-features/*/; do [ -d "$d" ] || continue; cp -r "$d" ".devcontainer/features/$(basename "$d")"; n=$((n+1)); done && echo "staged $n Features into .devcontainer/features"
platforms: [linux, darwin]
common:check:
desc: 'Fail if a Feature''s copy of a src/common helper (dcf-persist.sh) differs from the canonical file. Run `task common:sync` to refresh the copies.'
silent: true
cmds:
- cmd: |
rc=0
for canon in src/common/*.sh; do
name="$(basename "$canon")"
[ "$name" = install.sh ] && continue
for copy in src/*/"$name" local-features/*/"$name"; do
[ "$copy" = "$canon" ] && continue
if ! cmp -s "$canon" "$copy"; then echo "stale copy: $copy differs from $canon (task common:sync)"; rc=1; fi
done
done
[ "$rc" -eq 0 ] && echo "common helper copies are up to date"
exit "$rc"
taskfiles:check:
desc: 'Fail if a Feature Taskfile (src/<id>/taskfile.yml or local-features/<id>/taskfile.yml) breaks the mechanical rules: every task runs in the directory task was started from (dir: ''{{"{{"}}.USER_WORKING_DIR{{"}}"}}''; included through features.yml it would otherwise run in /usr/local/share/go-task/includes.d), every task has a desc, and the first task is version (NFR-2).'
silent: true
cmds:
- cmd: |
rc=0
for f in src/*/taskfile.yml local-features/*/taskfile.yml; do
found="$(awk -v q="'" '
function flush() {
if (task != "" && !dir) print "dir " task
if (task != "" && !desc) print "desc " task
task = ""; dir = 0; desc = 0
}
/^tasks:$/ { in_tasks = 1; next }
in_tasks && /^ [A-Za-z0-9:_-]+:$/ {
flush(); task = $1; sub(/:$/, "", task)
if (!first) { first = task; if (task != "version") print "first " task }
next
}
in_tasks && $0 == " dir: " q "{{"{{"}}.USER_WORKING_DIR{{"}}"}}" q { dir = 1 }
in_tasks && /^ desc: / { desc = 1 }
END { flush() }' "$f")"
while read -r kind t; do
case "$kind" in
dir) echo "$f: task $t has no dir: '{{"{{"}}.USER_WORKING_DIR{{"}}"}}'" ;;
desc) echo "$f: task $t has no desc (NFR-2)" ;;
first) echo "$f: the first task is $t, not version (NFR-2)" ;;
*) continue ;;
esac
rc=1
done <<EOF
$found
EOF
done
[ "$rc" -eq 0 ] && echo "every Feature task runs in the caller's directory, has a desc, and version comes first"
exit "$rc"
common:sync:
desc: 'Copy every src/common/*.sh helper over the copies the other Features carry (a Feature folder must be self-contained when published, so the helper is vendored rather than depended on).'
silent: true
cmds:
- cmd: |
for canon in src/common/*.sh; do
name="$(basename "$canon")"
[ "$name" = install.sh ] && continue
for copy in src/*/"$name" local-features/*/"$name"; do
[ "$copy" = "$canon" ] && continue
cp "$canon" "$copy" && echo "synced $copy"
done
done
repo:setup:
desc: 'Once per clone: activate the tracked .gitconfig (git config --local include.path ../.gitconfig), which turns on the repo hooks in .githooks/ (auto DCO sign-off) and the `git cs` alias. Touches only this clone''s .git/config, never your global config. Idempotent.'
silent: true
cmds:
- cmd: |
if [ "$(git config --local --get-all include.path | grep -cx '../.gitconfig')" = "0" ]; then
git config --local --add include.path ../.gitconfig && echo 'Activated .gitconfig (hooks + alias) for this clone.'
else
echo '.gitconfig already active for this clone.'
fi
- git config --get core.hooksPath
repo:govern:
desc: 'Apply the GitHub governance settings (security features, squash-only merges, Actions allowlist, release environment, protect-main ruleset) with gh - see .github/scripts/Set-RepoGovernance.ps1. Idempotent; needs gh logged in as a repo admin.'
cmds:
- pwsh -NoLogo -NoProfile -File '{{.ROOT_DIR}}/.github/scripts/Set-RepoGovernance.ps1'