Skip to content
This repository was archived by the owner on Oct 14, 2026. It is now read-only.
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
1 change: 1 addition & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
persist-credentials: false
- uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0
- run: just lint
- run: just lint-gh
env:
GH_TOKEN: ${{ github.token }} # zizmor online audits
- run: just validate
Expand Down
87 changes: 87 additions & 0 deletions .onedev-buildspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
version: 52
jobs:
- name: checks
jobExecutor: remote
steps:
- type: CheckoutStep
name: checkout
cloneCredential:
type: DefaultCredential
withLfs: false
withSubmodules: false
condition: SUCCESSFUL
optional: false
- type: CommandStep
name: lint and validate
runInContainer: true
image: docker:29.8.2-cli
interpreter:
type: PosixInterpreter
shell: sh
commands: |
set -eu
apk add --no-cache -q bash git jq python3 curl mise
git config --global --add safe.directory '*'
mise trust -q .mise.toml
mise install -q
mise exec -- just lint
mise exec -- just validate
runAs: 0:0
useTTY: true
condition: SUCCESSFUL
optional: false
triggers:
- type: PullRequestUpdateTrigger
branches: main
- type: BranchUpdateTrigger
branches: main
userMatch: anyone
retryCondition: never
maxRetries: 3
retryDelay: 30
timeout: 3600
- name: images
jobExecutor: remote
steps:
- type: CheckoutStep
name: checkout
cloneCredential:
type: DefaultCredential
withLfs: false
withSubmodules: false
condition: SUCCESSFUL
optional: false
- type: CommandStep
name: check tags, build, scan, smoke test
runInContainer: true
image: docker:29.8.2-cli
interpreter:
type: PosixInterpreter
shell: sh
commands: |
set -eu
apk add --no-cache -q bash git jq python3 curl
git config --global --add safe.directory '*'
# On a pull request the build runs on a merge commit: its first
# parent is the target branch, which check-tags compares against.
if [ "$(git rev-list --parents -n 1 HEAD | wc -w)" -gt 2 ]; then
bash scripts/check-tags.sh "$(git rev-parse HEAD^1)"
fi
bash scripts/ci-images.sh
runAs: 0:0
envVars:
- name: COMMIT
value: '@commit_hash@'
useTTY: true
condition: SUCCESSFUL
optional: false
triggers:
- type: PullRequestUpdateTrigger
branches: main
- type: BranchUpdateTrigger
branches: main
userMatch: anyone
retryCondition: never
maxRetries: 3
retryDelay: 30
timeout: 7200
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ can work with OneDev issues, pull requests and builds.
keeps tod below 4.3.3 (`renovate.json`). Remove that rule after the server
upgrade.

## OneDev CI (migration in progress)

The repository is moving to OneDev (`moveyourdigital/devops/coder-templates`).
Until the switch, GitHub stays the source of truth and `main` is pushed to
OneDev after each merge. `.onedev-buildspec.yml` (validate with
`tod build check-spec`), executor `remote`:

- `checks`: `just lint` + `just validate` (Alpine `docker:29.8.2-cli`, tools
from `.mise.toml`). `just lint-gh` (actionlint, zizmor) is GitHub only.
- `images`: on a pull request `scripts/check-tags.sh <target branch>`, then
`scripts/ci-images.sh`: build, trivy scan and smoke test of every image
with a new tag. **No push yet**: `main.tf` still points to GHCR.

Still to do: push to the OneDev registry, daily scan + rebuild PRs, Renovate
(built-in step), required builds, image pulls on x1, then archive GitHub.

## Pull request checks

`.github/workflows/checks.yml` runs on each pull request to `main`
Expand Down
47 changes: 7 additions & 40 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@ templates := "node-js php-wordpress ops"
default:
@just --list

# Check formatting, Dockerfiles, JSON, scripts, workflows, Renovate config and secrets
# Check formatting, Dockerfiles, JSON, scripts, Renovate config and secrets
lint:
terraform fmt -check -recursive
hadolint --config .hadolint.yaml */build/Dockerfile
for f in */build/*.json; do python3 -m json.tool "$f" >/dev/null || exit 1; done
shellcheck tests/*.sh scripts/*.sh
actionlint
check-jsonschema --builtin-schema vendor.renovate renovate.json
zizmor -q --no-progress --min-severity low .github/workflows
gitleaks dir . --no-banner --redact

# Check the GitHub workflows (GitHub only; removed after the OneDev move)
lint-gh:
actionlint
zizmor -q --no-progress --min-severity low .github/workflows

# terraform validate each template (in a temp copy: no lock file left behind)
validate:
#!/usr/bin/env bash
Expand All @@ -45,43 +48,7 @@ validate:
# A change under build/ needs a new image tag. A tag already in GHCR is
# accepted only if it was built from the same build/.
check-tags base="origin/main":
#!/usr/bin/env bash
set -euo pipefail
err() { if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::error file=$1::$2"; else echo "error: $2" >&2; fi; }
fail=0
for t in {{templates}}; do
new="$(just image "$t")"
old="$(git show "{{base}}:$t/main.tf" 2>/dev/null | scripts/image-ref.sh - 2>/dev/null || true)"
if [ "$new" = "$old" ]; then
if git diff --quiet "{{base}}...HEAD" -- "$t/build"; then
echo "ok: $t (unchanged: $new)"
else
err "$t/main.tf" "$t/build changed, but the image tag did not: $new"; fail=1
fi
else
# Fail closed: only an explicit "absent" counts as a new tag.
state="$(scripts/image-state.sh "$new")" || { err "$t/main.tf" "cannot check $new in GHCR"; fail=1; continue; }
if [ "$state" = present ]; then
# An existing tag is fine only if it was built from the same build/
# (security rebuild by scan.yml, or a rollback). Never re-pushed.
rev="$(scripts/image-revision.sh "$new")" || { err "$t/main.tf" "cannot read the revision of $new"; fail=1; continue; }
if git cat-file -e "$rev^{commit}" 2>/dev/null && git diff --quiet "$rev" HEAD -- "$t/build"; then
# The build workflow skips existing tags: scan it here, it may
# have CVEs found after it was built.
if docker pull -q "$new" >/dev/null && scripts/trivy-scan.sh "$new"; then
echo "ok: $t (existing tag, same build/ as ${rev:0:7}, trivy clean: $new)"
else
err "$t/main.tf" "existing tag has fixable CRITICAL vulnerabilities (or the scan failed): $new"; fail=1
fi
else
err "$t/main.tf" "tag already in GHCR, built from a different build/ (${rev:0:7}): $new"; fail=1
fi
else
echo "ok: $t (new tag: $new)"
fi
fi
done
exit "$fail"
scripts/check-tags.sh "{{base}}"

# Image used by a template (from main.tf)
image t:
Expand Down
44 changes: 44 additions & 0 deletions scripts/check-tags.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# A change under <template>/build/ needs a new image tag. A tag already in
# the registry is accepted only if it was built from the same build/ (and its
# image is trivy-clean). Used by `just check-tags` and by CI.
#
# scripts/check-tags.sh <base commit>
set -euo pipefail

base="${1:?base commit}"
err() { if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::error file=$1::$2"; else echo "error: $2" >&2; fi; }
fail=0
for t in node-js php-wordpress ops; do
new="$(scripts/image-ref.sh "$t/main.tf")"
old="$(git show "$base:$t/main.tf" 2>/dev/null | scripts/image-ref.sh - 2>/dev/null || true)"
if [ "$new" = "$old" ]; then
if git diff --quiet "$base...HEAD" -- "$t/build"; then
echo "ok: $t (unchanged: $new)"
else
err "$t/main.tf" "$t/build changed, but the image tag did not: $new"; fail=1
fi
else
# Fail closed: only an explicit "absent" counts as a new tag.
state="$(scripts/image-state.sh "$new")" || { err "$t/main.tf" "cannot check $new in the registry"; fail=1; continue; }
if [ "$state" = present ]; then
# An existing tag is fine only if it was built from the same build/
# (security rebuild, or a rollback). Never re-pushed.
rev="$(scripts/image-revision.sh "$new")" || { err "$t/main.tf" "cannot read the revision of $new"; fail=1; continue; }
if git cat-file -e "$rev^{commit}" 2>/dev/null && git diff --quiet "$rev" HEAD -- "$t/build"; then
# The build workflow skips existing tags: scan it here, it may
# have CVEs found after it was built.
if docker pull -q "$new" >/dev/null && scripts/trivy-scan.sh "$new"; then
echo "ok: $t (existing tag, same build/ as ${rev:0:7}, trivy clean: $new)"
else
err "$t/main.tf" "existing tag has fixable CRITICAL vulnerabilities (or the scan failed): $new"; fail=1
fi
else
err "$t/main.tf" "tag already in the registry, built from a different build/ (${rev:0:7}): $new"; fail=1
fi
else
echo "ok: $t (new tag: $new)"
fi
fi
done
exit "$fail"
45 changes: 45 additions & 0 deletions scripts/ci-images.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Build, scan and smoke-test the workspace images; push with --push.
# Same steps as .github/workflows/build-images.yml, for OneDev CI.
# A tag that already exists in the registry is skipped (tags are immutable),
# after a check that it was built from the same build/.
#
# scripts/ci-images.sh [--push]
#
# Env: COMMIT (revision label, default HEAD), SOURCE_URL (source label).
set -euo pipefail

push=false
if [ "${1:-}" = --push ]; then push=true; fi
rev="${COMMIT:-$(git rev-parse HEAD)}"

for t in node-js php-wordpress ops; do
img="$(scripts/image-ref.sh "$t/main.tf")"
echo "== $t: $img"

# Fails (exit 2) on registry errors: never build/push on a guess.
state="$(scripts/image-state.sh "$img")"
if [ "$state" = present ]; then
built_from="$(scripts/image-revision.sh "$img")"
if ! git cat-file -e "$built_from^{commit}" 2>/dev/null \
|| ! git diff --quiet "$built_from" HEAD -- "$t/build"; then
echo "error: $img exists but was built from a different $t/build/ (${built_from:0:7}). Use a new tag." >&2
exit 1
fi
echo "Tag exists (same build/), nothing to build."
continue
fi

docker buildx build --load --pull --platform linux/amd64 \
--label "org.opencontainers.image.revision=$rev" \
--label "org.opencontainers.image.source=${SOURCE_URL:-}" \
--label "org.opencontainers.image.title=coder-workspace-$t" \
-t "$img" "$t/build"
scripts/trivy-scan.sh "$img"
tests/smoke.sh "$t" "$img"
if $push; then
docker push "$img"
else
echo "Built, scanned and smoke-tested (no push)."
fi
done
Loading