0eb88de8f2
Block integration-tester contamination artifacts / Block staging-trigger / invalid manifest contamination (pull_request) Successful in 12s
sop-checklist / all-items-acked (pull_request) ack advisory until agent-team — would be failure: acked: 0/10 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +7 — body
sop-checklist / na-declarations (pull_request) N/A: (none)
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
sop-checklist / all-items-acked (pull_request_target) Successful in 5s
CI / Python Lint & Test (pull_request) Successful in 16s
E2E Staging SaaS (full lifecycle) / E2E Staging Platform Boot (pull_request) Successful in 9s
E2E Staging SaaS (full lifecycle) / E2E Staging SaaS (pull_request) Successful in 12s
ci-required-drift / drift (pull_request) Successful in 21s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 21s
Concierge Creates Workspace Hermetic / Concierge Creates Workspace Hermetic (pull_request) Successful in 23s
E2E Staging SaaS (full lifecycle) / pr-validate (pull_request) Successful in 22s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge user_tasks (pull_request) Successful in 16s
E2E Staging SaaS (full lifecycle) / E2E Staging Workspace Requests (core#2606) (pull_request) Successful in 16s
Harness Replays / detect-changes (pull_request) Successful in 10s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Creates Workspace (pull_request) Successful in 16s
CI / Detect changes (pull_request) Successful in 34s
E2E API Smoke Test / detect-changes (pull_request) Successful in 34s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 15s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 39s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge Platform Agent (pull_request) Successful in 21s
E2E Chat / detect-changes (pull_request) Successful in 40s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 10s
E2E Staging SaaS (full lifecycle) / E2E Staging Concierge (compile+skip) (pull_request) Successful in 24s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 9s
E2E Staging SaaS (full lifecycle) / E2E Staging Plugin Install Lifecycle (pull_request) Successful in 22s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 10s
lint-bp-context-emit-match / lint-bp-context-emit-match (pull_request) Successful in 21s
lint-continue-on-error-tracking / lint-continue-on-error-tracking (pull_request) Successful in 27s
lint-required-workflows-docker-host-pinned / Lint docker-host pin on docker-touching workflows (pull_request) Successful in 17s
lint-phantom-gate / lint-phantom-gate (pull_request) Successful in 24s
Lint forbidden hand-written mcp__ tool-id literals / Scan for hand-written mcp__ tool-id literals (pull_request) Successful in 29s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 18s
Harness Replays / Harness Replays (pull_request) Has been cancelled
lint-no-coe-on-required / lint-no-coe-on-required (pull_request) Successful in 47s
Lint publish-runner timeout-minutes / Lint publish-runner timeout-minutes (pull_request) Successful in 47s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 46s
lint-setup-go-cache / lint-setup-go-cache (pull_request) Successful in 33s
Lint schedule budget / Zero-cron ratchet (pull_request) Successful in 43s
Lint pre-flip continue-on-error / Verify continue-on-error flips have run-log proof (pull_request) Successful in 50s
lint-staging-tenant-cd-gate-chain / lint-staging-tenant-cd-gate-chain (pull_request) Successful in 35s
Lint workflow YAML (Gitea-1.22.6-hostile shapes) / Lint workflow YAML for Gitea-1.22.6-hostile shapes (pull_request) Successful in 35s
sop-checklist / review-refire (pull_request_target) Successful in 4s
lint-env-coupling-dismissal / lint-env-coupling-dismissal (pull_request) Successful in 1m4s
gate-check-v3 / gate-check (pull_request_target) Successful in 11s
CI / Platform (Go) (pull_request) Successful in 4s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
lint-required-context-exists-in-bp / lint-required-context-exists-in-bp (pull_request) Successful in 58s
E2E Staging SaaS (full lifecycle) / Prune stale e2e DNS records (pull_request) Successful in 8s
template-delivery-e2e / detect-changes (pull_request) Successful in 20s
CI / Canvas (Next.js) (pull_request) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 5s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
E2E Chat / E2E Chat (pull_request) Successful in 6s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 10s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 5s
PR Diff Guard / PR diff guard (pull_request_target) Successful in 19s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 50s
CI / Canvas Deploy Status (pull_request) Successful in 4s
template-delivery-e2e / Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile) (pull_request) Successful in 6s
CI / all-required (pull_request) Successful in 9s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Successful in 1m4s
gitea-merge-queue / queue (pull_request_review) Successful in 1m30s
qa-review / approved (pull_request_target) review gate disabled_manually (bot-approval ceremony not required); 2 official APPROVEs present
security-review / approved (pull_request_target) review gate disabled_manually (bot-approval ceremony not required); 2 official APPROVEs present
reserved-path-review / reserved-path-review (pull_request_target) review gate disabled_manually (bot-approval ceremony not required); 2 official APPROVEs present
audit-force-merge / audit (pull_request_target) Successful in 10s
The `Teardown dind` step was `if: always()` (ungated), so on a NO-OP commit — where detect-changes says run/delivery != 'true' and every real step (incl. Start dind) is skipped — it still ran, chdir'd into working-directory tests/harness in the no-op checkout context, and failed (OCI runtime exec: chdir ... no such file or directory → exit 127). Observed on template-delivery-e2e for this workflow-only diff. Gate it like the existing Force teardown (`always() && needs.detect-changes.outputs. run/delivery == 'true'`): the no-op path never starts a dind, so there is nothing to reap and the tests/harness cwd is absent. When the job DOES run, always()+flag still tears the dind down on any failure. A SIGKILL-cancelled run leaves at most one uniquely-named dind-harness-* container for the age-guarded reaper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
275 lines
15 KiB
YAML
275 lines
15 KiB
YAML
name: template-delivery-e2e
|
|
|
|
# RFC #2843 regression gate — asserts a fresh tenant DELIVERS + SERVES BACK the
|
|
# rendered /configs bundle (config.yaml + prompts). This is the gate that would
|
|
# have caught the 2026-06-15 incident family (#2919 model/identity drop, #32
|
|
# skill drop) and the #206 docker-less read-back 59-byte stub.
|
|
#
|
|
# ── ROOT-FIX (deploy-ordering deadlock) ────────────────────────────────────
|
|
# The old gate provisioned against DEPLOYED staging (MOLECULE_CP_URL=
|
|
# staging-api) and asserted config.yaml. That is a class-D ENV-COUPLING bug: a
|
|
# delivery-surface PR could NEVER green its own gate until it was deployed — but
|
|
# deploying needed the merge the red gate blocked (circular). It froze #3478
|
|
# (the #206 read-back fix) and the staging 59B regression with it.
|
|
#
|
|
# The gate now validates the PR's OWN artifact:
|
|
# 1. Builds the tenant image (workspace-server/Dockerfile.tenant) FROM THIS REF
|
|
# on robot-1 (`runs-on: [docker-host, robot-1]`) — off the local box.
|
|
# 2. Brings up the production-shape harness (tests/harness) — the SAME image
|
|
# shipped to prod, running as a real molecules-server tenant (MOLECULE_ORG_ID
|
|
# set → CPProvisioner, docker-less), talking to the harness cp-stub.
|
|
# 3. Provisions a FRESH seo-agent workspace THROUGH the tenant, then asserts
|
|
# the tenant SERVES BACK the FULL rendered config.yaml (>1 KiB) + prompts
|
|
# via the Files API — the #206 host-side /configs mirror read-back. A PR
|
|
# that regresses delivery serves the 59-byte stub → the gate fails.
|
|
# 4. Tears the harness down (ephemeral; reaper-clean; NO staging org touched,
|
|
# NO staging pin touched).
|
|
# So the gate validates the PR's CHANGE with no deployed-staging dependency.
|
|
#
|
|
# The post-online PLUGIN channel (seo-all reconcile, assertion E) INHERENTLY
|
|
# needs a live deployed runtime (the harness cp-stub never brings one online),
|
|
# so it CANNOT be a pre-merge PR gate by construction. It is preserved,
|
|
# unchanged, in template-delivery-e2e-staging.yml (`workflow_dispatch`, run
|
|
# post-deploy against staging) — no assertion was dropped, it was relocated off
|
|
# the PR critical path.
|
|
#
|
|
# Required-workflow shape (unchanged): NO `on: paths:` filter (a required check
|
|
# must not be path-filtered — a docs-only PR would wedge forever on an absent
|
|
# context). The detect-changes job applies the delivery path-scoping at RUNTIME
|
|
# (profile `template-delivery`, in detect-changes.py); the delivery job's real
|
|
# steps are gated per-step on its output. A non-delivery PR emits SUCCESS
|
|
# cheaply (no build); a delivery PR runs the full PR-build e2e and BLOCKS on
|
|
# failure. Mirrors e2e-api / peer-visibility / harness-replays.
|
|
|
|
on:
|
|
workflow_dispatch: {}
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
concurrency:
|
|
group: template-delivery-e2e-${{ github.event.pull_request.head.sha || github.sha }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
GITHUB_SERVER_URL: https://git.moleculesai.app
|
|
|
|
jobs:
|
|
# Runtime path-scoping (replaces the removed `on: paths:`). Outputs
|
|
# `delivery=true` when the diff touches the workspace-server delivery surface;
|
|
# the gate job runs the real PR-build e2e only then. bp-exempt: helper job;
|
|
# the REQUIRED context is the `delivery` job below, not this one.
|
|
detect-changes:
|
|
runs-on: docker-host
|
|
continue-on-error: false
|
|
outputs:
|
|
delivery: ${{ steps.decide.outputs.delivery }}
|
|
debug: ${{ steps.decide.outputs.debug }}
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
fetch-depth: 0
|
|
- id: decide
|
|
env:
|
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
PUSH_BEFORE: ${{ github.event.before }}
|
|
run: |
|
|
python3 .gitea/scripts/detect-changes.py \
|
|
--profile template-delivery \
|
|
--event-name "${{ github.event_name }}" \
|
|
--pr-base-sha "$PR_BASE_SHA" \
|
|
--base-ref "$PR_BASE_REF" \
|
|
--push-before "$PUSH_BEFORE" || {
|
|
# Script crash → fail OPEN so the gate runs rather than silently
|
|
# no-oping a potentially-breaking delivery PR.
|
|
echo "delivery=true" >> "$GITHUB_OUTPUT"
|
|
echo "debug=detect-script-error event=${{ github.event_name }}" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
}
|
|
echo "debug=profile=template-delivery event=${{ github.event_name }}" >> "$GITHUB_OUTPUT"
|
|
|
|
# ONE job (no job-level `if:`) that ALWAYS runs and reports under the
|
|
# required-check name. Real work is gated per-step on
|
|
# `needs.detect-changes.outputs.delivery` — a non-delivery PR runs only the
|
|
# no-op step and emits SUCCESS (branch-protection-clean; a job-level `if:`
|
|
# would emit a SKIPPED check run that fails the required-check eval — see
|
|
# e2e-api.yml's PR#2264 note). A delivery PR runs the full PR-build e2e and,
|
|
# with no continue-on-error, BLOCKS the merge on failure.
|
|
# bp-required: yes — this context is merge-blocking; branch protection lists
|
|
# "<this> (pull_request)" (via the ["*"] all-green gate). Name UNCHANGED from
|
|
# the pre-redesign gate so the required-context identity does not drift (the
|
|
# asset-channel config+prompts assertions it names are exactly what the
|
|
# PR-build path asserts; the "seo-all via plugin reconcile" clause moved to
|
|
# template-delivery-e2e-staging.yml — see this file's header).
|
|
delivery:
|
|
needs: detect-changes
|
|
# No colon in the name — lint-required-context's PyYAML AST parse rejects an
|
|
# unquoted scalar containing a colon.
|
|
name: Template-asset delivery (fresh seo-agent — config+prompts via asset channel, seo-all via plugin reconcile)
|
|
# robot-1's 32c dedicated build box (`[docker-host, robot-1]` = AND, so it
|
|
# lands ONLY on robot-1 while satisfying the docker-host-pinned lint). Keeps
|
|
# the heavy tenant-image build + harness e2e OFF the local molecules-server
|
|
# box (operator-config#373 local-box SSOT).
|
|
runs-on: [docker-host, robot-1]
|
|
# Build (cache-warm minutes; cold ~45) + harness up + provision + read-back.
|
|
timeout-minutes: 75
|
|
steps:
|
|
- name: No-op pass (delivery surface unchanged in this diff)
|
|
if: needs.detect-changes.outputs.delivery != 'true'
|
|
run: |
|
|
echo "No delivery-surface changes — gate satisfied without provisioning."
|
|
echo "::notice::template-delivery-e2e no-op pass (detect-changes: ${{ needs.detect-changes.outputs.debug }})."
|
|
|
|
- if: needs.detect-changes.outputs.delivery == 'true'
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Log detected changes
|
|
if: needs.detect-changes.outputs.delivery == 'true'
|
|
run: |
|
|
echo "::notice::detect-changes debug ${{ needs.detect-changes.outputs.debug }}"
|
|
|
|
# AUTO_SYNC_TOKEN (devops-engineer persona PAT) is (a) the cross-repo clone
|
|
# token for the private workspace-template-* deps in manifest.json, and
|
|
# (b) wired into the tenant env as MOLECULE_TEMPLATE_REPO_TOKEN so the
|
|
# tenant's giteaTemplateAssetFetcher can fetch the PRIVATE seo-agent
|
|
# template at provision time. Sourced from the Infisical SSOT
|
|
# (/shared/dev-utils, environment=prod) via the CI machine identity.
|
|
#
|
|
# FORK SAFETY: gated to same-repo events — a fork PR must never harvest the
|
|
# token. On a fork the step is skipped, the fetch falls back to anonymous
|
|
# clone (public deps), and the private seo-agent fetch would fail loudly in
|
|
# the replay (not silently) — a maintainer re-runs from a trusted ref.
|
|
- name: Fetch AUTO_SYNC_TOKEN from Infisical SSOT
|
|
id: auto_sync_token
|
|
if: >-
|
|
needs.detect-changes.outputs.delivery == 'true' &&
|
|
(github.event_name == 'workflow_dispatch' ||
|
|
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
|
|
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository))
|
|
env:
|
|
INFISICAL_CI_CLIENT_ID: ${{ secrets.INFISICAL_CI_CLIENT_ID }}
|
|
INFISICAL_CI_CLIENT_SECRET: ${{ secrets.INFISICAL_CI_CLIENT_SECRET }}
|
|
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_CI_PROJECT_ID }}
|
|
run: |
|
|
set -uo pipefail
|
|
BASE="https://key.moleculesai.app"
|
|
TOK=$(curl -fsS -X POST "$BASE/api/v1/auth/universal-auth/login" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"clientId\":\"$INFISICAL_CI_CLIENT_ID\",\"clientSecret\":\"$INFISICAL_CI_CLIENT_SECRET\"}" \
|
|
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=d.get("accessToken"); sys.stdout.write(v if isinstance(v,str) and v else "")')
|
|
read_secret() {
|
|
curl -fsS "$BASE/api/v3/secrets/raw/$1?workspaceId=$INFISICAL_PROJECT_ID&environment=prod&secretPath=$2" \
|
|
-H "Authorization: Bearer $TOK" \
|
|
| python3 -c 'import sys,json; d=json.load(sys.stdin); v=(d.get("secret") or {}).get("secretValue"); sys.stdout.write(v if isinstance(v,str) and v else "")'
|
|
}
|
|
AUTO_SYNC_TOKEN=$(read_secret AUTO_SYNC_TOKEN %2Fshared%2Fdev-utils)
|
|
echo "::add-mask::$AUTO_SYNC_TOKEN"
|
|
if [ -z "$AUTO_SYNC_TOKEN" ]; then
|
|
echo "::error::Infisical returned empty for AUTO_SYNC_TOKEN at /shared/dev-utils"
|
|
exit 1
|
|
fi
|
|
echo "AUTO_SYNC_TOKEN=$AUTO_SYNC_TOKEN" >> "$GITHUB_ENV"
|
|
echo "AUTO_SYNC_TOKEN loaded from Infisical (len=${#AUTO_SYNC_TOKEN})"
|
|
|
|
# Pre-clone manifest deps before docker compose builds the tenant image
|
|
# (Dockerfile.tenant COPYs .tenant-bundle-deps/* — private repos can't be
|
|
# cloned inside the build sandbox). Same pattern as harness-replays.yml.
|
|
- name: Pre-clone manifest deps
|
|
if: needs.detect-changes.outputs.delivery == 'true'
|
|
env:
|
|
MOLECULE_GITEA_TOKEN: ${{ env.AUTO_SYNC_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${MOLECULE_GITEA_TOKEN}" ]; then
|
|
echo "::warning::AUTO_SYNC_TOKEN not set — anonymous clone (public deps only; private seo-agent will fail the read-back assertion loudly on a fork)"
|
|
fi
|
|
mkdir -p .tenant-bundle-deps
|
|
sed '/^[[:space:]]*\/\//d' manifest.json > .manifest-stripped.json
|
|
bash scripts/clone-manifest.sh \
|
|
.manifest-stripped.json \
|
|
.tenant-bundle-deps/workspace-configs-templates \
|
|
.tenant-bundle-deps/org-templates \
|
|
.tenant-bundle-deps/plugins
|
|
ws_count=$(find .tenant-bundle-deps/workspace-configs-templates -mindepth 1 -maxdepth 1 -type d | wc -l)
|
|
echo "Cloned workspace-configs-templates: $ws_count"
|
|
|
|
- name: Install Python deps for replays
|
|
if: needs.detect-changes.outputs.delivery == 'true'
|
|
run: |
|
|
pip install \
|
|
--extra-index-url https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple/ \
|
|
-r tests/harness/requirements.txt
|
|
|
|
# Build the harness images FROM THIS REF explicitly. `docker compose up`
|
|
# reuses an existing image and does NOT rebuild on a source change — on a
|
|
# persistent robot-1 daemon that would test a STALE tenant image. A
|
|
# cache-aware `build` rebuilds only the layers whose inputs changed (the
|
|
# COPY of workspace-server source invalidates on any delivery change), so
|
|
# THIS ref's code is always what runs. GIT_SHA is baked so /buildinfo
|
|
# reports the served SHA (verify-don't-buy).
|
|
- name: Start disposable dind (per-job isolation — task #78)
|
|
# Same pattern as harness-replays.yml: run the harness stack inside a
|
|
# throwaway docker:dind so a cancelled/SIGKILLed run can't orphan harness-*
|
|
# containers/volumes on the shared host and concurrent runs can't collide
|
|
# on the fixed compose project/ports. dind.sh exports DOCKER_HOST (every
|
|
# `docker compose` below targets the ISOLATED daemon) + BASE/CP_STUB_BASE
|
|
# (the buildinfo curl + template-asset-delivery-gate.sh reach the nested
|
|
# harness via host-loopback forwards) + HARNESS_BIND_ADDR. Torn down in the
|
|
# always() step at the end.
|
|
if: needs.detect-changes.outputs.delivery == 'true'
|
|
working-directory: tests/harness
|
|
run: bash dind.sh up
|
|
|
|
- name: Build harness images FROM THIS REF (explicit, not up's skip-if-exists)
|
|
if: needs.detect-changes.outputs.delivery == 'true'
|
|
working-directory: tests/harness
|
|
env:
|
|
SECRETS_ENCRYPTION_KEY: build-placeholder
|
|
GIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
run: docker compose -f compose.yml build tenant-alpha tenant-beta cp-stub
|
|
|
|
- name: Bring up harness + run template-asset delivery gate
|
|
if: needs.detect-changes.outputs.delivery == 'true'
|
|
timeout-minutes: 20
|
|
working-directory: tests/harness
|
|
env:
|
|
GIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
# Wire the private-template fetch token so the tenant can fetch the
|
|
# PRIVATE seo-agent template at provision time.
|
|
MOLECULE_TEMPLATE_REPO_TOKEN: ${{ env.AUTO_SYNC_TOKEN }}
|
|
run: |
|
|
set -uo pipefail
|
|
./up.sh
|
|
echo "--- buildinfo (served SHA must match this ref) ---"
|
|
curl -sS -H "Host: harness-tenant-alpha.localhost" "${BASE:-http://localhost:8080}/buildinfo" | head -c 300 || true
|
|
echo ""
|
|
echo "--- running template-asset delivery gate ---"
|
|
bash template-asset-delivery-gate.sh
|
|
|
|
- name: Dump tenant logs on failure
|
|
if: failure() && needs.detect-changes.outputs.delivery == 'true'
|
|
working-directory: tests/harness
|
|
env:
|
|
SECRETS_ENCRYPTION_KEY: dump-logs-placeholder
|
|
run: |
|
|
echo "=== docker compose ps ==="; docker compose -f compose.yml ps || true
|
|
echo "=== tenant-alpha logs (last 200) ==="; docker compose -f compose.yml logs --tail 200 tenant-alpha || true
|
|
echo "=== cp-stub logs (last 80) ==="; docker compose -f compose.yml logs --tail 80 cp-stub || true
|
|
|
|
- name: Force teardown
|
|
if: always() && needs.detect-changes.outputs.delivery == 'true'
|
|
working-directory: tests/harness
|
|
run: ./down.sh || true
|
|
|
|
- name: Teardown dind (destroys the whole harness topology atomically)
|
|
# LAST step — the dump + ./down.sh steps above run against DOCKER_HOST=dind.
|
|
# Removing the single dind destroys every nested container/volume/network in
|
|
# one atomic op on the HOST daemon (dind.sh down unsets DOCKER_HOST). Gated
|
|
# like Force teardown on delivery=='true': the no-op path never starts a
|
|
# dind (nothing to reap), and its checkout lacks the tests/harness cwd.
|
|
if: always() && needs.detect-changes.outputs.delivery == 'true'
|
|
working-directory: tests/harness
|
|
run: bash dind.sh down
|