@meffecta/agent 1.0.3 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -70
- package/bin/meffecta-agent.js +65 -13
- package/deployment.env.example +24 -11
- package/engine.json +2 -2
- package/lib/analytics.js +262 -0
- package/lib/commands.js +225 -40
- package/lib/create-job.js +344 -0
- package/lib/doctor.js +20 -7
- package/lib/gcloud.js +25 -0
- package/lib/integrations.js +3 -3
- package/lib/resources.js +300 -0
- package/lib/verify-credentials.js +129 -0
- package/lib/version.js +90 -0
- package/package.json +4 -2
- package/scripts/create-project.sh +3 -1
- package/scripts/deploy.sh +19 -8
- package/scripts/lib/cli-names.sh +55 -0
- package/scripts/lib/deployment.sh +18 -3
- package/scripts/lib/read-json.mjs +1 -1
- package/scripts/lib/tasks-queue.sh +24 -0
- package/scripts/link-billing.sh +1 -1
- package/scripts/mint-gmail-token.mjs +7 -2
- package/scripts/mint-graph-token.mjs +7 -2
- package/scripts/set-secret.sh +1 -1
- package/scripts/setup-infrastructure.sh +187 -100
- package/scripts/{setup-scheduler.sh → sync-triggers.sh} +12 -16
- package/scripts/update-tooling.sh +3 -1
- package/scripts/verify-credentials.mjs +168 -49
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# What to call the next command, in output an operator reads. Sourced, never run.
|
|
2
|
+
#
|
|
3
|
+
# Almost every operator reaches these scripts through the CLI — `meffecta-agent deploy` —
|
|
4
|
+
# and has never seen a .sh file: the engine's repo is private, so a deployment run by
|
|
5
|
+
# someone outside Meffecta cannot clone it, and the scripts arrive vendored inside the npm
|
|
6
|
+
# package or lifted out of the image. Printing "run ./scripts/deploy.sh next" names a file
|
|
7
|
+
# they do not have.
|
|
8
|
+
#
|
|
9
|
+
# The CLI exports MEFFECTA_CLI with the invocation it was reached by, which is both the
|
|
10
|
+
# name to print and the proof that a CLI is there at all. Without it, the operator is
|
|
11
|
+
# running the file directly and the filename IS the answer.
|
|
12
|
+
#
|
|
13
|
+
# This is its own lib rather than part of deployment.sh because create-project.sh and
|
|
14
|
+
# update-tooling.sh deliberately avoid the deployment config — they run before there is
|
|
15
|
+
# one, or beside it — and still have to name their next step.
|
|
16
|
+
|
|
17
|
+
CLI_LIB_SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
18
|
+
|
|
19
|
+
# cmd <subcommand> → "meffecta-agent deploy", or "/path/to/scripts/deploy.sh"
|
|
20
|
+
cmd() {
|
|
21
|
+
if [ -n "${MEFFECTA_CLI:-}" ]; then
|
|
22
|
+
printf '%s %s' "${MEFFECTA_CLI}" "$1"
|
|
23
|
+
return
|
|
24
|
+
fi
|
|
25
|
+
local file
|
|
26
|
+
case "$1" in
|
|
27
|
+
setup-infra) file="setup-infrastructure.sh" ;;
|
|
28
|
+
artifact-cleanup) file="set-artifact-cleanup.sh" ;;
|
|
29
|
+
mint-gmail) file="mint-gmail-token.mjs" ;;
|
|
30
|
+
mint-graph) file="mint-graph-token.mjs" ;;
|
|
31
|
+
verify-credentials) file="verify-credentials.mjs" ;;
|
|
32
|
+
*) file="$1.sh" ;;
|
|
33
|
+
esac
|
|
34
|
+
printf '%s/%s' "${CLI_LIB_SCRIPTS_DIR}" "${file}"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# The same for the running script naming itself — "run this again from there". $0 is an
|
|
38
|
+
# absolute path inside node_modules when the CLI spawned it, which is not a thing anyone
|
|
39
|
+
# would type.
|
|
40
|
+
self_cmd() {
|
|
41
|
+
[ -n "${MEFFECTA_CLI:-}" ] || { printf '%s' "$0"; return; }
|
|
42
|
+
local base
|
|
43
|
+
base="$(basename "$0")"
|
|
44
|
+
case "${base}" in
|
|
45
|
+
setup-infrastructure.sh) printf '%s setup-infra' "${MEFFECTA_CLI}" ;;
|
|
46
|
+
set-artifact-cleanup.sh) printf '%s artifact-cleanup' "${MEFFECTA_CLI}" ;;
|
|
47
|
+
mint-gmail-token.mjs) printf '%s mint-gmail' "${MEFFECTA_CLI}" ;;
|
|
48
|
+
mint-graph-token.mjs) printf '%s mint-graph' "${MEFFECTA_CLI}" ;;
|
|
49
|
+
*) printf '%s %s' "${MEFFECTA_CLI}" "${base%.*}" ;;
|
|
50
|
+
esac
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# True when the operator has the CLI, i.e. when there is a `meffecta-agent doctor` to point
|
|
54
|
+
# at. Guards the lines that only make sense with one.
|
|
55
|
+
has_cli() { [ -n "${MEFFECTA_CLI:-}" ]; }
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
# Config file format is plain KEY=value lines, # comments allowed. See
|
|
23
23
|
# deployment.env.example.
|
|
24
24
|
|
|
25
|
+
# What to call the next command in output: cmd / self_cmd / has_cli.
|
|
26
|
+
. "$(dirname "${BASH_SOURCE[0]}")/cli-names.sh"
|
|
27
|
+
|
|
25
28
|
# Consume --config <file> from a script's arguments before it parses its own.
|
|
26
29
|
# Usage: eval "$(extract_config_flag "$@")" — sets CONFIG_FILE and rewrites "$@".
|
|
27
30
|
CONFIG_FILE=""
|
|
@@ -62,6 +65,13 @@ resolve_deployment() {
|
|
|
62
65
|
REGION="${REGION:-europe-west1}"
|
|
63
66
|
ARTIFACT_REPO="${ARTIFACT_REPO:-${SERVICE:-agent}-images}"
|
|
64
67
|
GHCR_IMAGE="${GHCR_IMAGE:-ghcr.io/meffecta/agent}"
|
|
68
|
+
# How the engine image reaches Cloud Run, which can only ever pull from Artifact
|
|
69
|
+
# Registry. `proxy` makes ARTIFACT_REPO a remote repository in front of ghcr.io, so AR
|
|
70
|
+
# fetches on demand and no local docker is involved at all. `mirror` is the older path:
|
|
71
|
+
# pull from ghcr.io on the operator's machine, retag, push — which needs docker, and
|
|
72
|
+
# pushes several gigabytes for every engine version.
|
|
73
|
+
REGISTRY_MODE="${REGISTRY_MODE:-proxy}"
|
|
74
|
+
GHCR_PATH="${GHCR_IMAGE#*/}" # ghcr.io/meffecta/agent → meffecta/agent
|
|
65
75
|
TIMEZONE="${TIMEZONE:-Europe/Stockholm}"
|
|
66
76
|
|
|
67
77
|
# The runtime shape, asserted by deploy.sh on every rollout so the deployed service
|
|
@@ -85,10 +95,15 @@ No deployment.env found in $(pwd).
|
|
|
85
95
|
That file lives in the deployment's content repo (committed — none of it is
|
|
86
96
|
secret), so the usual fix is to run this from there:
|
|
87
97
|
|
|
88
|
-
cd /path/to/your-content-repo && $
|
|
98
|
+
cd /path/to/your-content-repo && $(self_cmd)
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
$(if has_cli; then
|
|
101
|
+
echo "Creating one: ${MEFFECTA_CLI} init, in that repo."
|
|
102
|
+
else
|
|
103
|
+
echo "Creating one: copy deployment.env.example (it ships beside these scripts)"
|
|
104
|
+
echo "beside your jobs/."
|
|
105
|
+
fi)
|
|
106
|
+
Or point at a file explicitly with --config /path/to/deployment.env.
|
|
92
107
|
EOF
|
|
93
108
|
exit 2
|
|
94
109
|
fi
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Pull single values out of the JSON that
|
|
2
|
+
// Pull single values out of the JSON that sync-triggers.sh reads from gcloud and from the
|
|
3
3
|
// running service. Shell cannot parse JSON and node is already required by the operator
|
|
4
4
|
// tooling (mint-gmail-token.mjs, verify-credentials.mjs), so this replaces what used to be
|
|
5
5
|
// inline python3 — one fewer thing a deployment has to have installed.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# The Cloud Tasks queue that carries a scale-to-zero deployment's work back to it.
|
|
2
|
+
#
|
|
3
|
+
# Two scripts create it, which is why it lives here rather than in either of them.
|
|
4
|
+
# setup-infrastructure.sh provisions it with the deployment's other static resources — it
|
|
5
|
+
# is ordinary infrastructure, and knows nothing about jobs. sync-triggers.sh ensures it
|
|
6
|
+
# too, for a deployment provisioned before that was true and for the standalone re-sync.
|
|
7
|
+
# Duplicating the settings in both would mean a deployment's retry behaviour depending on
|
|
8
|
+
# which script last touched it.
|
|
9
|
+
#
|
|
10
|
+
# From the caller: GC (gcloud with --project), REGION, RUNTIME_SA, and a run() wrapper that
|
|
11
|
+
# honours DRY_RUN.
|
|
12
|
+
ensure_tasks_queue() {
|
|
13
|
+
local queue="$1"
|
|
14
|
+
if ! ${GC} tasks queues describe "${queue}" --location="${REGION}" >/dev/null 2>&1; then
|
|
15
|
+
run ${GC} tasks queues create "${queue}" --location="${REGION}" --quiet
|
|
16
|
+
fi
|
|
17
|
+
# max-concurrent-dispatches leaves room for several held attempts at once; the engine's
|
|
18
|
+
# own queue is serial, so this bounds requests waiting, not jobs running.
|
|
19
|
+
run ${GC} tasks queues update "${queue}" --location="${REGION}" \
|
|
20
|
+
--max-attempts=4 --min-backoff=5s --max-backoff=30s --max-doublings=2 \
|
|
21
|
+
--max-concurrent-dispatches=8 --max-dispatches-per-second=2 --quiet
|
|
22
|
+
run ${GC} tasks queues add-iam-policy-binding "${queue}" --location="${REGION}" \
|
|
23
|
+
--member="serviceAccount:${RUNTIME_SA}" --role="roles/cloudtasks.enqueuer" --quiet
|
|
24
|
+
}
|
package/scripts/link-billing.sh
CHANGED
|
@@ -136,7 +136,7 @@ if [ "$(gcloud billing projects describe "${PROJECT}" --format='value(billingEna
|
|
|
136
136
|
echo "✔ billingEnabled: true"
|
|
137
137
|
echo ""
|
|
138
138
|
echo "Next: create the content repo (step 3), then provision from inside it —"
|
|
139
|
-
echo " $(
|
|
139
|
+
echo " $(cmd setup-infra)"
|
|
140
140
|
else
|
|
141
141
|
echo "✖ Linked, but billingEnabled is still false — check the account is open and funded." >&2
|
|
142
142
|
exit 1
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Sign in as WHICHEVER account the token is for when the browser opens — run once
|
|
17
17
|
// per account (private Gmail → GMAIL_REFRESH_TOKEN; additional accounts →
|
|
18
18
|
// --account NAME → GMAIL_<NAME>_REFRESH_TOKEN). --readonly mints read scopes only,
|
|
19
|
-
// which is the right level for an account
|
|
19
|
+
// which is the right level for an account this deployment doesn't administer alone (a
|
|
20
20
|
// company mailbox: read the mail, never send from that identity).
|
|
21
21
|
// Gmail scopes are "restricted", so an unverified app
|
|
22
22
|
// shows a warning — Advanced → continue is fine for your own account. A Workspace
|
|
@@ -120,7 +120,12 @@ if (!res.ok || !json.refresh_token) {
|
|
|
120
120
|
process.exit(1);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// How the operator reached this script, so the line telling them to pipe it into
|
|
124
|
+
// gcloud is one they can actually type. MEFFECTA_CLI is set when the CLI spawned us;
|
|
125
|
+
// without it they ran the file directly, and the file path is the honest answer.
|
|
126
|
+
const self = process.env.MEFFECTA_CLI ? `${process.env.MEFFECTA_CLI} mint-gmail` : `node ${process.argv[1]}`;
|
|
127
|
+
|
|
123
128
|
// Refresh token to stdout (pipe straight into gcloud), everything else to stderr.
|
|
124
129
|
console.error(`\nRefresh token minted — it is ${tokenVar}. Store it without it touching your shell history:`);
|
|
125
|
-
console.error(`
|
|
130
|
+
console.error(` ${self} ... | gcloud secrets create ${tokenVar} --data-file=-\n`);
|
|
126
131
|
console.log(json.refresh_token);
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// 4. Note the Application (client) ID and the Directory (tenant) ID.
|
|
27
27
|
//
|
|
28
28
|
// Usage:
|
|
29
|
-
// node scripts/mint-graph-token.mjs --account
|
|
29
|
+
// node scripts/mint-graph-token.mjs --account WORK --tenant <tenant-id> \
|
|
30
30
|
// --client-id <id> --client-secret <secret> [--no-calendar]
|
|
31
31
|
//
|
|
32
32
|
// Sign in as the mailbox the token is for when the browser opens — the everyday
|
|
@@ -146,7 +146,12 @@ if (!res.ok || !json.refresh_token) {
|
|
|
146
146
|
process.exit(1);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// How the operator reached this script, so the line telling them to pipe it into
|
|
150
|
+
// gcloud is one they can actually type. MEFFECTA_CLI is set when the CLI spawned us;
|
|
151
|
+
// without it they ran the file directly, and the file path is the honest answer.
|
|
152
|
+
const self = process.env.MEFFECTA_CLI ? `${process.env.MEFFECTA_CLI} mint-graph` : `node ${process.argv[1]}`;
|
|
153
|
+
|
|
149
154
|
// Refresh token to stdout (pipe straight into gcloud), everything else to stderr.
|
|
150
155
|
console.error(`\nRefresh token minted — it is ${tokenVar}. Store it without it touching your shell history:`);
|
|
151
|
-
console.error(`
|
|
156
|
+
console.error(` ${self} ... | gcloud secrets create ${tokenVar} --data-file=-\n`);
|
|
152
157
|
console.log(json.refresh_token);
|
package/scripts/set-secret.sh
CHANGED
|
@@ -35,7 +35,7 @@ while [ $# -gt 0 ]; do
|
|
|
35
35
|
esac
|
|
36
36
|
done
|
|
37
37
|
|
|
38
|
-
[ -n "${NAME}" ] || { echo "Which secret? e.g.
|
|
38
|
+
[ -n "${NAME}" ] || { echo "Which secret? e.g. $(self_cmd) GITHUB_TOKEN" >&2; exit 2; }
|
|
39
39
|
case "${NAME}" in
|
|
40
40
|
[A-Z]*[A-Z0-9]|[A-Z]) ;;
|
|
41
41
|
*) echo "Secret names are UPPER_SNAKE_CASE: got '${NAME}'" >&2; exit 2 ;;
|
|
@@ -14,20 +14,79 @@ set -euo pipefail
|
|
|
14
14
|
#
|
|
15
15
|
# Which deployment it provisions comes from ./deployment.env (or --config <file>) —
|
|
16
16
|
# see deployment.env.example. Nothing is defaulted to another deployment's project.
|
|
17
|
+
#
|
|
18
|
+
# ... setup-infra # provision
|
|
19
|
+
# ... setup-infra --dry-run # print every change it would make, touch nothing
|
|
20
|
+
#
|
|
21
|
+
# --dry-run still READS the project, so it reports what it would do to the project you
|
|
22
|
+
# actually have — what is missing, what is already there — rather than a fixed list of
|
|
23
|
+
# steps. Nothing it prints needs billing, which is also what makes this path testable
|
|
24
|
+
# before anyone runs it for real.
|
|
17
25
|
|
|
18
26
|
. "$(dirname "${BASH_SOURCE[0]}")/lib/deployment.sh"
|
|
27
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib/tasks-queue.sh"
|
|
28
|
+
|
|
29
|
+
DRY_RUN=false
|
|
19
30
|
|
|
20
31
|
while [ $# -gt 0 ]; do
|
|
21
32
|
case "$1" in
|
|
22
33
|
--config) CONFIG_FILE="${2:?--config needs a file}"; shift 2 ;;
|
|
34
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
23
35
|
-h|--help) sed -n '4,25p' "$0"; exit 0 ;;
|
|
24
36
|
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
|
25
37
|
esac
|
|
26
38
|
done
|
|
27
39
|
|
|
40
|
+
# Every call that CHANGES something goes through this; the describe calls that ask what is
|
|
41
|
+
# already there do not, so a dry run reads a real project and reports what it would do to
|
|
42
|
+
# it rather than a fixed script of steps.
|
|
43
|
+
#
|
|
44
|
+
# This is the script that creates a service account and grants it roles inside someone
|
|
45
|
+
# else's GCP project, run by an operator who cannot read it — it is published tooling, and
|
|
46
|
+
# the repo behind it is private. Being able to see exactly what it will do, first, is the
|
|
47
|
+
# least a vendor can offer for that. It also makes the client path testable without a
|
|
48
|
+
# project, which is the only way it gets exercised at all before a client runs it.
|
|
49
|
+
run() {
|
|
50
|
+
if [ "${DRY_RUN}" = true ]; then
|
|
51
|
+
echo " (dry run) $*"
|
|
52
|
+
return 0
|
|
53
|
+
fi
|
|
54
|
+
"$@"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# The same, for calls whose success output is a full IAM policy dump nobody reads. The
|
|
58
|
+
# redirect has to live HERE rather than on the call, or it swallows the dry-run line too —
|
|
59
|
+
# which it did, leaving every IAM grant, the most consequential thing this script does, as
|
|
60
|
+
# the one thing a dry run did not show.
|
|
61
|
+
run_quiet() {
|
|
62
|
+
if [ "${DRY_RUN}" = true ]; then
|
|
63
|
+
echo " (dry run) $*"
|
|
64
|
+
return 0
|
|
65
|
+
fi
|
|
66
|
+
"$@" >/dev/null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Deploys are not wired to the engine repo's CI: it publishes images to GHCR, and a
|
|
70
|
+
# deployment rolls one out when it chooses. The deployer SA + Workload Identity
|
|
71
|
+
# Federation below are therefore optional — provision them only if THIS deployment wants
|
|
72
|
+
# a CI deploy job of its own, from its own content repo:
|
|
73
|
+
# SETUP_CI_DEPLOY=true GITHUB_REPO=<owner>/<your-content-repo> ...
|
|
74
|
+
#
|
|
75
|
+
# GITHUB_REPO has NO DEFAULT, and that is the point. The value becomes a trust condition:
|
|
76
|
+
# the named repo's GitHub Actions may impersonate the deployer SA, which holds run.admin
|
|
77
|
+
# on this project. A default would hand that to whoever the default names — someone else
|
|
78
|
+
# entirely — on behalf of an operator who never typed a repo at all.
|
|
79
|
+
SETUP_CI_DEPLOY="${SETUP_CI_DEPLOY:-false}"
|
|
80
|
+
GITHUB_REPO="${GITHUB_REPO:-}" # owner/repo a CI deploy would run from
|
|
81
|
+
if [ "${SETUP_CI_DEPLOY}" = "true" ] && ! printf '%s' "${GITHUB_REPO}" | grep -Eq '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$'; then
|
|
82
|
+
echo "SETUP_CI_DEPLOY=true needs GITHUB_REPO=<owner>/<repo> — the repo allowed to deploy." >&2
|
|
83
|
+
echo "It is written into a trust condition, so there is nothing safe to guess." >&2
|
|
84
|
+
exit 2
|
|
85
|
+
fi
|
|
86
|
+
|
|
28
87
|
require_gcloud
|
|
29
88
|
resolve_deployment
|
|
30
|
-
announce_target "Provisioning infrastructure"
|
|
89
|
+
announce_target "Provisioning infrastructure$([ "${DRY_RUN}" = true ] && echo " (dry run — reads only)")"
|
|
31
90
|
|
|
32
91
|
ACCOUNT="${ACCOUNT:-}"
|
|
33
92
|
MEMORY_BUCKET="${PROJECT}-memory"
|
|
@@ -36,22 +95,13 @@ RUNTIME_SA_NAME="agent-runtime"
|
|
|
36
95
|
RUNTIME_SA="${RUNTIME_SA_NAME}@${PROJECT}.iam.gserviceaccount.com"
|
|
37
96
|
DEPLOY_SA_NAME="github-deployer"
|
|
38
97
|
DEPLOY_SA="${DEPLOY_SA_NAME}@${PROJECT}.iam.gserviceaccount.com"
|
|
39
|
-
# Deploys are not wired to the engine repo's CI: it publishes images to GHCR, and a
|
|
40
|
-
# deployment rolls one out when it chooses (scripts/deploy.sh). The deployer SA +
|
|
41
|
-
# Workload Identity Federation below are therefore optional — provision them only if
|
|
42
|
-
# THIS deployment wants a CI deploy job of its own, from its content repo:
|
|
43
|
-
# SETUP_CI_DEPLOY=true GITHUB_REPO=meffecta/agent-home ./scripts/setup-infrastructure.sh
|
|
44
|
-
SETUP_CI_DEPLOY="${SETUP_CI_DEPLOY:-false}"
|
|
45
|
-
GITHUB_REPO="${GITHUB_REPO:-meffecta/agent-home}" # owner/repo a CI deploy would run from
|
|
46
|
-
|
|
47
98
|
# ACCOUNT is optional: unset means whichever account gcloud is currently logged in as.
|
|
48
99
|
GC="gcloud --project=${PROJECT}"
|
|
49
100
|
[ -n "${ACCOUNT}" ] && GC="${GC} --account=${ACCOUNT}"
|
|
50
101
|
|
|
51
102
|
echo "🔧 Enabling APIs..."
|
|
52
|
-
${GC} services enable \
|
|
103
|
+
run ${GC} services enable \
|
|
53
104
|
run.googleapis.com \
|
|
54
|
-
cloudbuild.googleapis.com \
|
|
55
105
|
artifactregistry.googleapis.com \
|
|
56
106
|
secretmanager.googleapis.com \
|
|
57
107
|
iamcredentials.googleapis.com \
|
|
@@ -69,19 +119,50 @@ ${GC} services enable \
|
|
|
69
119
|
analyticsdata.googleapis.com \
|
|
70
120
|
iam.googleapis.com
|
|
71
121
|
|
|
72
|
-
echo "🐳 Artifact Registry repo..."
|
|
122
|
+
echo "🐳 Artifact Registry repo (${REGISTRY_MODE})..."
|
|
73
123
|
if ! ${GC} artifacts repositories describe "${ARTIFACT_REPO}" --location="${REGION}" &>/dev/null; then
|
|
74
|
-
|
|
124
|
+
if [ "${REGISTRY_MODE}" = "proxy" ]; then
|
|
125
|
+
# A remote repository standing in front of ghcr.io. Cloud Run can only pull from
|
|
126
|
+
# Artifact Registry, and this is how that requirement is met without a local docker:
|
|
127
|
+
# AR fetches the public engine image itself, on demand, and caches it. No upstream
|
|
128
|
+
# credentials — the engine package is public, which is what makes this the simple path.
|
|
129
|
+
run ${GC} artifacts repositories create "${ARTIFACT_REPO}" --location="${REGION}" \
|
|
130
|
+
--repository-format=docker --mode=remote-repository \
|
|
131
|
+
--remote-docker-repo="https://${GHCR_IMAGE%%/*}" \
|
|
132
|
+
--description="Proxy of ${GHCR_IMAGE%%/*} — the engine image is fetched on demand"
|
|
133
|
+
else
|
|
134
|
+
run ${GC} artifacts repositories create "${ARTIFACT_REPO}" --location="${REGION}" --repository-format=docker
|
|
135
|
+
fi
|
|
136
|
+
else
|
|
137
|
+
ACTUAL_MODE=$(${GC} artifacts repositories describe "${ARTIFACT_REPO}" --location="${REGION}" --format='value(mode)')
|
|
138
|
+
case "${ACTUAL_MODE}:${REGISTRY_MODE}" in
|
|
139
|
+
REMOTE_REPOSITORY:proxy|STANDARD_REPOSITORY:mirror) ;;
|
|
140
|
+
*)
|
|
141
|
+
# A repository's mode is fixed at creation, so this is not something to "fix" here —
|
|
142
|
+
# deploy.sh would resolve an image path the repository cannot serve, and the failure
|
|
143
|
+
# would arrive as a Cloud Run pull error with nothing pointing back to this.
|
|
144
|
+
echo "⚠ ${ARTIFACT_REPO} is a ${ACTUAL_MODE} but REGISTRY_MODE=${REGISTRY_MODE}." >&2
|
|
145
|
+
echo " A repository's mode cannot be changed. Either set REGISTRY_MODE to match, or" >&2
|
|
146
|
+
echo " name a different ARTIFACT_REPO in deployment.env and re-run this." >&2
|
|
147
|
+
;;
|
|
148
|
+
esac
|
|
75
149
|
fi
|
|
76
150
|
# The images here are a cache of ghcr.io/meffecta/agent, not an archive, so they are worth
|
|
77
151
|
# expiring — see set-artifact-cleanup.sh for why that is safe and what the policy keeps.
|
|
78
|
-
|
|
152
|
+
# Captured rather than streamed: gcloud narrates this one on stderr ("Updated repository",
|
|
153
|
+
# "Dry run is disabled"), which reads like something went wrong in the middle of a set-up.
|
|
154
|
+
# Housekeeping, so a failure is a warning rather than the end of provisioning.
|
|
155
|
+
if ! CLEANUP_LOG=$("$(dirname "${BASH_SOURCE[0]}")/set-artifact-cleanup.sh" ${CONFIG_FILE:+--config "${CONFIG_FILE}"} ${DRY_RUN:+$([ "${DRY_RUN}" = true ] && echo --dry-run)} 2>&1); then
|
|
156
|
+
echo "${CLEANUP_LOG}" >&2
|
|
157
|
+
echo "⚠ Could not set the registry cleanup policy — old images will accumulate." >&2
|
|
158
|
+
fi
|
|
79
159
|
|
|
80
160
|
# --- Runtime service account (everything the agent itself can do) ---
|
|
81
161
|
if ! ${GC} iam service-accounts describe "${RUNTIME_SA}" &>/dev/null; then
|
|
82
162
|
echo "🆕 Creating runtime service account: ${RUNTIME_SA}"
|
|
83
|
-
${GC} iam service-accounts create "${RUNTIME_SA_NAME}" --display-name="Meffecta Agent"
|
|
84
|
-
|
|
163
|
+
run ${GC} iam service-accounts create "${RUNTIME_SA_NAME}" --display-name="Meffecta Agent"
|
|
164
|
+
# IAM takes a moment to propagate; a dry run has nothing to wait for.
|
|
165
|
+
[ "${DRY_RUN}" = true ] || for _ in $(seq 1 12); do
|
|
85
166
|
${GC} iam service-accounts describe "${RUNTIME_SA}" &>/dev/null && break
|
|
86
167
|
sleep 5
|
|
87
168
|
done
|
|
@@ -90,33 +171,20 @@ fi
|
|
|
90
171
|
# --- Buckets: memory (mounted, agent-writable) + audit (write-once, never mounted) ---
|
|
91
172
|
if ! ${GC} storage buckets describe "gs://${MEMORY_BUCKET}" &>/dev/null; then
|
|
92
173
|
echo "🆕 Creating memory bucket: ${MEMORY_BUCKET}"
|
|
93
|
-
${GC} storage buckets create "gs://${MEMORY_BUCKET}" --location="${REGION}" --uniform-bucket-level-access
|
|
174
|
+
run ${GC} storage buckets create "gs://${MEMORY_BUCKET}" --location="${REGION}" --uniform-bucket-level-access
|
|
94
175
|
fi
|
|
95
176
|
if ! ${GC} storage buckets describe "gs://${AUDIT_BUCKET}" &>/dev/null; then
|
|
96
177
|
echo "🆕 Creating audit bucket: ${AUDIT_BUCKET} (365d retention)"
|
|
97
|
-
${GC} storage buckets create "gs://${AUDIT_BUCKET}" --location="${REGION}" --uniform-bucket-level-access \
|
|
178
|
+
run ${GC} storage buckets create "gs://${AUDIT_BUCKET}" --location="${REGION}" --uniform-bucket-level-access \
|
|
98
179
|
--retention-period=365d
|
|
99
180
|
fi
|
|
100
|
-
# Cloud Build source staging — only for a deployment that builds images in GCP rather
|
|
101
|
-
# than pulling the published one. Source tarballs are transient, so a 30-day lifecycle
|
|
102
|
-
# keeps it from growing.
|
|
103
|
-
CLOUDBUILD_BUCKET="${PROJECT}-cloudbuild"
|
|
104
|
-
if [ "${SETUP_CI_DEPLOY}" = "true" ] && ! ${GC} storage buckets describe "gs://${CLOUDBUILD_BUCKET}" &>/dev/null; then
|
|
105
|
-
echo "🆕 Creating Cloud Build staging bucket: ${CLOUDBUILD_BUCKET}"
|
|
106
|
-
${GC} storage buckets create "gs://${CLOUDBUILD_BUCKET}" --location="${REGION}" --uniform-bucket-level-access
|
|
107
|
-
LIFECYCLE_FILE=$(mktemp)
|
|
108
|
-
printf '{"rule":[{"action":{"type":"Delete"},"condition":{"age":30}}]}' >"${LIFECYCLE_FILE}"
|
|
109
|
-
${GC} storage buckets update "gs://${CLOUDBUILD_BUCKET}" --lifecycle-file="${LIFECYCLE_FILE}"
|
|
110
|
-
rm -f "${LIFECYCLE_FILE}"
|
|
111
|
-
fi
|
|
112
|
-
|
|
113
181
|
echo "🔐 Runtime IAM..."
|
|
114
|
-
${GC} storage buckets add-iam-policy-binding "gs://${MEMORY_BUCKET}" \
|
|
115
|
-
--member="serviceAccount:${RUNTIME_SA}" --role="roles/storage.objectAdmin" --quiet
|
|
116
|
-
${GC} storage buckets add-iam-policy-binding "gs://${AUDIT_BUCKET}" \
|
|
117
|
-
--member="serviceAccount:${RUNTIME_SA}" --role="roles/storage.objectCreator" --quiet
|
|
118
|
-
${GC} projects add-iam-policy-binding "${PROJECT}" \
|
|
119
|
-
--member="serviceAccount:${RUNTIME_SA}" --role="roles/secretmanager.secretAccessor" --quiet
|
|
182
|
+
run_quiet ${GC} storage buckets add-iam-policy-binding "gs://${MEMORY_BUCKET}" \
|
|
183
|
+
--member="serviceAccount:${RUNTIME_SA}" --role="roles/storage.objectAdmin" --quiet
|
|
184
|
+
run_quiet ${GC} storage buckets add-iam-policy-binding "gs://${AUDIT_BUCKET}" \
|
|
185
|
+
--member="serviceAccount:${RUNTIME_SA}" --role="roles/storage.objectCreator" --quiet
|
|
186
|
+
run_quiet ${GC} projects add-iam-policy-binding "${PROJECT}" \
|
|
187
|
+
--member="serviceAccount:${RUNTIME_SA}" --role="roles/secretmanager.secretAccessor" --quiet
|
|
120
188
|
|
|
121
189
|
PROJECT_NUMBER=$(${GC} projects describe "${PROJECT}" --format='value(projectNumber)')
|
|
122
190
|
|
|
@@ -124,51 +192,46 @@ PROJECT_NUMBER=$(${GC} projects describe "${PROJECT}" --format='value(projectNum
|
|
|
124
192
|
if [ "${SETUP_CI_DEPLOY}" = "true" ]; then
|
|
125
193
|
if ! ${GC} iam service-accounts describe "${DEPLOY_SA}" &>/dev/null; then
|
|
126
194
|
echo "🆕 Creating deployer service account: ${DEPLOY_SA}"
|
|
127
|
-
${GC} iam service-accounts create "${DEPLOY_SA_NAME}" --display-name="GitHub Actions deployer"
|
|
128
|
-
for _ in $(seq 1 12); do
|
|
195
|
+
run ${GC} iam service-accounts create "${DEPLOY_SA_NAME}" --display-name="GitHub Actions deployer"
|
|
196
|
+
[ "${DRY_RUN}" = true ] || for _ in $(seq 1 12); do
|
|
129
197
|
${GC} iam service-accounts describe "${DEPLOY_SA}" &>/dev/null && break
|
|
130
198
|
sleep 5
|
|
131
199
|
done
|
|
132
200
|
fi
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
--member="serviceAccount:${CLOUDBUILD_SA}" --role="roles/iam.serviceAccountUser" --quiet >/dev/null
|
|
145
|
-
# `gcloud builds submit` runs the build as the default compute SA: the deployer must be
|
|
146
|
-
# allowed to act as it, and it needs to read staged source, push images, and deploy.
|
|
147
|
-
${GC} iam service-accounts add-iam-policy-binding "${CLOUDBUILD_SA}" \
|
|
148
|
-
--member="serviceAccount:${DEPLOY_SA}" --role="roles/iam.serviceAccountUser" --quiet >/dev/null
|
|
149
|
-
for role in roles/cloudbuild.builds.builder roles/artifactregistry.writer roles/run.admin; do
|
|
150
|
-
${GC} projects add-iam-policy-binding "${PROJECT}" \
|
|
151
|
-
--member="serviceAccount:${CLOUDBUILD_SA}" --role="${role}" --quiet >/dev/null
|
|
201
|
+
# What a DEPLOY needs, and nothing a BUILD would: roll out a revision, pull (or in mirror
|
|
202
|
+
# mode push) the engine image, and read this project's enabled services.
|
|
203
|
+
#
|
|
204
|
+
# Deliberately NOT granted: secretmanager.secretAccessor. deploy.sh re-syncs the Cloud
|
|
205
|
+
# Scheduler triggers, and that step reads the API secret — so a CI deploy runs
|
|
206
|
+
# `deploy --no-scheduler`, and an operator re-runs sync-triggers by hand when a cron
|
|
207
|
+
# changes. The alternative is a CI identity that can read every credential the deployment
|
|
208
|
+
# holds, which is a poor trade for skipping one occasional command.
|
|
209
|
+
for role in roles/run.admin roles/artifactregistry.writer roles/serviceusage.serviceUsageConsumer; do
|
|
210
|
+
run_quiet ${GC} projects add-iam-policy-binding "${PROJECT}" \
|
|
211
|
+
--member="serviceAccount:${DEPLOY_SA}" --role="${role}" --quiet
|
|
152
212
|
done
|
|
213
|
+
# Deploying a service that runs as the runtime SA means acting as it, once.
|
|
214
|
+
run_quiet ${GC} iam service-accounts add-iam-policy-binding "${RUNTIME_SA}" \
|
|
215
|
+
--member="serviceAccount:${DEPLOY_SA}" --role="roles/iam.serviceAccountUser" --quiet
|
|
153
216
|
|
|
154
217
|
POOL_ID="github-pool"
|
|
155
218
|
PROVIDER_ID="github-provider"
|
|
156
219
|
if ! ${GC} iam workload-identity-pools describe "${POOL_ID}" --location=global &>/dev/null; then
|
|
157
220
|
echo "🆕 Creating Workload Identity pool + provider for GitHub Actions..."
|
|
158
|
-
${GC} iam workload-identity-pools create "${POOL_ID}" --location=global --display-name="GitHub Actions"
|
|
221
|
+
run ${GC} iam workload-identity-pools create "${POOL_ID}" --location=global --display-name="GitHub Actions"
|
|
159
222
|
fi
|
|
160
223
|
if ! ${GC} iam workload-identity-pools providers describe "${PROVIDER_ID}" \
|
|
161
224
|
--location=global --workload-identity-pool="${POOL_ID}" &>/dev/null; then
|
|
162
|
-
${GC} iam workload-identity-pools providers create-oidc "${PROVIDER_ID}" \
|
|
225
|
+
run ${GC} iam workload-identity-pools providers create-oidc "${PROVIDER_ID}" \
|
|
163
226
|
--location=global --workload-identity-pool="${POOL_ID}" \
|
|
164
227
|
--display-name="GitHub OIDC" \
|
|
165
228
|
--issuer-uri="https://token.actions.githubusercontent.com" \
|
|
166
229
|
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
|
|
167
230
|
--attribute-condition="assertion.repository == '${GITHUB_REPO}'"
|
|
168
231
|
fi
|
|
169
|
-
${GC} iam service-accounts add-iam-policy-binding "${DEPLOY_SA}" \
|
|
232
|
+
run_quiet ${GC} iam service-accounts add-iam-policy-binding "${DEPLOY_SA}" \
|
|
170
233
|
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.repository/${GITHUB_REPO}" \
|
|
171
|
-
--role="roles/iam.workloadIdentityUser" --quiet
|
|
234
|
+
--role="roles/iam.workloadIdentityUser" --quiet
|
|
172
235
|
else
|
|
173
236
|
echo "⏭️ Skipping deployer SA + Workload Identity Federation (SETUP_CI_DEPLOY is not true)."
|
|
174
237
|
fi
|
|
@@ -176,14 +239,44 @@ fi
|
|
|
176
239
|
# --- Cloud Run service shell (placeholder image; scripts/deploy.sh rolls out a real one) ---
|
|
177
240
|
if ! ${GC} run services describe "${SERVICE}" --region="${REGION}" &>/dev/null; then
|
|
178
241
|
echo "🆕 Creating Cloud Run service: ${SERVICE}"
|
|
179
|
-
${GC} run services deploy "${SERVICE}" --region="${REGION}" \
|
|
242
|
+
run ${GC} run services deploy "${SERVICE}" --region="${REGION}" \
|
|
180
243
|
--image="gcr.io/cloudrun/hello" --allow-unauthenticated \
|
|
181
244
|
--service-account="${RUNTIME_SA}" 2>/dev/null ||
|
|
182
|
-
${GC} run deploy "${SERVICE}" --region="${REGION}" \
|
|
245
|
+
run ${GC} run deploy "${SERVICE}" --region="${REGION}" \
|
|
183
246
|
--image="gcr.io/cloudrun/hello" --allow-unauthenticated \
|
|
184
247
|
--service-account="${RUNTIME_SA}"
|
|
185
248
|
fi
|
|
186
249
|
|
|
250
|
+
# The transport for a deployment that scales to zero. It is static infrastructure — it
|
|
251
|
+
# knows nothing about jobs — so it belongs here with everything else that is created once,
|
|
252
|
+
# rather than in sync-triggers.sh, which cannot run until an engine is deployed and can
|
|
253
|
+
# be asked what jobs exist.
|
|
254
|
+
#
|
|
255
|
+
# Setting AGENT_TASKS_QUEUE here is also what lets `deploy` create the Cloud Scheduler
|
|
256
|
+
# triggers on the FIRST rollout: it syncs them for a deployment that has opted into
|
|
257
|
+
# external triggers, and these two variables are that opt-in. Without this the two waited
|
|
258
|
+
# on each other and a deployment needed a separate sync-triggers run to break the tie.
|
|
259
|
+
#
|
|
260
|
+
# Only for scale-to-zero. An always-on deployment drives itself with in-process timers, and
|
|
261
|
+
# giving it external triggers as well would fire every cron twice.
|
|
262
|
+
TRIGGER_ENV=""
|
|
263
|
+
if [ "${SCALING}" = "scale-to-zero" ]; then
|
|
264
|
+
QUEUE="${QUEUE_NAME:-${SERVICE}-runs}"
|
|
265
|
+
echo "📥 Cloud Tasks queue ${QUEUE}..."
|
|
266
|
+
ensure_tasks_queue "${QUEUE}"
|
|
267
|
+
AGENT_URL=$(${GC} run services describe "${SERVICE}" --region="${REGION}" \
|
|
268
|
+
--format='value(status.url)' 2>/dev/null || true)
|
|
269
|
+
if [ -z "${AGENT_URL}" ] && [ "${DRY_RUN}" = true ]; then
|
|
270
|
+
AGENT_URL="<the service URL, once it exists>"
|
|
271
|
+
fi
|
|
272
|
+
if [ -n "${AGENT_URL}" ]; then
|
|
273
|
+
TRIGGER_ENV=",AGENT_TASKS_QUEUE=projects/${PROJECT}/locations/${REGION}/queues/${QUEUE},AGENT_PUBLIC_URL=${AGENT_URL}"
|
|
274
|
+
else
|
|
275
|
+
# Not fatal: sync-triggers.sh sets both, and deploy runs it once they are there.
|
|
276
|
+
echo " ⚠️ Could not read the service URL — $(cmd sync-triggers) will set these."
|
|
277
|
+
fi
|
|
278
|
+
fi
|
|
279
|
+
|
|
187
280
|
echo "⚙️ Applying service settings (instances, memory volume, env)..."
|
|
188
281
|
VOLUME_FLAGS=()
|
|
189
282
|
if ! ${GC} run services describe "${SERVICE}" --region="${REGION}" --format=yaml 2>/dev/null | grep -q "name: memory"; then
|
|
@@ -194,7 +287,7 @@ fi
|
|
|
194
287
|
#
|
|
195
288
|
# Scale to zero means CPU billed only while a request is open. That is affordable because
|
|
196
289
|
# the service is idle almost all day, and it is only correct because nothing here fires on
|
|
197
|
-
# its own: scripts/
|
|
290
|
+
# its own: scripts/sync-triggers.sh gives the deployment its external triggers, and every
|
|
198
291
|
# run happens inside a request the handler holds open (src/keyed-runs.ts). REQUEST_TIMEOUT
|
|
199
292
|
# is the ceiling on one such held request; --cpu-boost pays for the cold start each wake-up
|
|
200
293
|
# costs, which for this service includes cloning the content repo. MAX_INSTANCES=1 keeps
|
|
@@ -206,58 +299,52 @@ case "${SCALING}" in
|
|
|
206
299
|
always-on) SCALE_FLAGS=(--min-instances=1 --no-cpu-throttling) ;;
|
|
207
300
|
*) echo "Unknown SCALING=\"${SCALING}\" — use scale-to-zero or always-on." >&2; exit 2 ;;
|
|
208
301
|
esac
|
|
209
|
-
${GC} run services update "${SERVICE}" --region="${REGION}" \
|
|
302
|
+
run ${GC} run services update "${SERVICE}" --region="${REGION}" \
|
|
210
303
|
--service-account="${RUNTIME_SA}" \
|
|
211
304
|
--max-instances="${MAX_INSTANCES}" \
|
|
212
305
|
"${SCALE_FLAGS[@]}" \
|
|
213
306
|
--memory="${MEMORY}" --cpu="${CPU}" --timeout="${REQUEST_TIMEOUT}" \
|
|
214
307
|
--execution-environment=gen2 \
|
|
215
308
|
${VOLUME_FLAGS[@]+"${VOLUME_FLAGS[@]}"} \
|
|
216
|
-
--update-env-vars="TIMEZONE=${TIMEZONE},AUDIT_BUCKET=${AUDIT_BUCKET}"
|
|
309
|
+
--update-env-vars="TIMEZONE=${TIMEZONE},AUDIT_BUCKET=${AUDIT_BUCKET}${TRIGGER_ENV}"
|
|
217
310
|
|
|
218
311
|
echo ""
|
|
219
312
|
echo "============================================================"
|
|
220
|
-
|
|
221
|
-
echo ""
|
|
222
|
-
|
|
223
|
-
echo "
|
|
224
|
-
|
|
225
|
-
echo " GITHUB_TOKEN (below) must be able to clone it, and every repo its jobs name."
|
|
226
|
-
echo ""
|
|
227
|
-
echo " 2. Gmail OAuth (once): in this project's console — OAuth consent screen"
|
|
228
|
-
echo " (External, PUBLISH to production) + a Desktop-app OAuth client. Then per"
|
|
229
|
-
echo " account: node scripts/mint-gmail-token.mjs --client-id ... --client-secret ..."
|
|
230
|
-
echo " [--account <NAME>] [--readonly] (sign in as that account)."
|
|
313
|
+
if [ "${DRY_RUN}" = true ]; then
|
|
314
|
+
echo "🔍 Dry run — nothing above was created, changed or granted."
|
|
315
|
+
else
|
|
316
|
+
echo "✅ Infrastructure ready."
|
|
317
|
+
fi
|
|
231
318
|
echo ""
|
|
232
|
-
echo "
|
|
233
|
-
echo "
|
|
234
|
-
echo "
|
|
235
|
-
echo "
|
|
236
|
-
echo " Required: AGENT_API_SECRET, CLAUDE_CODE_OAUTH_TOKEN (claude setup-token), GITHUB_TOKEN"
|
|
237
|
-
echo " Gmail: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN — and one"
|
|
238
|
-
echo " GMAIL_<NAME>_REFRESH_TOKEN per extra account (e.g. GMAIL_MEVISIO_REFRESH_TOKEN)"
|
|
239
|
-
echo " Optional: GRAFANA_LOGS_HOST/USERNAME + GRAFANA_API_KEY"
|
|
240
|
-
echo " Plain vars (--update-env-vars, not secrets): GMAIL_ADDRESS, GMAIL_<NAME>_ADDRESS"
|
|
319
|
+
echo " Created: the Google APIs, the registry the engine image is served from, the login"
|
|
320
|
+
echo " the agent acts as, a bucket for its memory, a write-once bucket for the audit"
|
|
321
|
+
echo " trail, the queue that carries work back to the service, and the service itself —"
|
|
322
|
+
echo " empty, waiting for an engine."
|
|
241
323
|
echo ""
|
|
242
|
-
echo "
|
|
243
|
-
echo " PROJECT=${PROJECT} REGION=${REGION} SERVICE=${SERVICE} ./scripts/deploy.sh"
|
|
244
|
-
echo " (defaults to the newest published image; --tag sha-<short> pins one)"
|
|
245
|
-
echo " Content changes need no deploy at all — they are live on the next run."
|
|
324
|
+
echo " Next, from this directory:"
|
|
246
325
|
echo ""
|
|
247
|
-
echo "
|
|
248
|
-
echo "
|
|
249
|
-
echo "
|
|
250
|
-
echo "
|
|
251
|
-
echo "
|
|
326
|
+
echo " $(cmd set-env) GIT_REPO_URL=https://github.com/<owner>/<content-repo>.git"
|
|
327
|
+
echo " $(cmd set-secret) AGENT_API_SECRET --random"
|
|
328
|
+
echo " $(cmd set-secret) CLAUDE_CODE_OAUTH_TOKEN # from: claude setup-token"
|
|
329
|
+
echo " $(cmd set-secret) GITHUB_TOKEN # must be able to clone the repo above"
|
|
330
|
+
echo " $(cmd deploy) # rolls out the engine, then creates its triggers"
|
|
331
|
+
if has_cli; then
|
|
332
|
+
echo ""
|
|
333
|
+
echo " Then give it one ability at a time — mail, analytics, a CRM, a database:"
|
|
334
|
+
echo ""
|
|
335
|
+
echo " ${MEFFECTA_CLI} connect # lists them; connect <name> walks through one"
|
|
336
|
+
echo ""
|
|
337
|
+
echo " ${MEFFECTA_CLI} steps the whole set-up, in order"
|
|
338
|
+
echo " ${MEFFECTA_CLI} doctor what is still missing, once something is running"
|
|
339
|
+
fi
|
|
252
340
|
if [ "${SETUP_CI_DEPLOY}" = "true" ]; then
|
|
253
341
|
WIF_PROVIDER="projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}"
|
|
254
342
|
echo ""
|
|
255
343
|
echo " 6. For a CI deploy from ${GITHUB_REPO} → Settings → Secrets → Actions:"
|
|
256
344
|
echo " GCP_WORKLOAD_IDENTITY_PROVIDER = ${WIF_PROVIDER}"
|
|
257
345
|
echo " GCP_DEPLOY_SERVICE_ACCOUNT = ${DEPLOY_SA}"
|
|
346
|
+
echo " That identity may deploy and nothing else — it cannot read your secrets, so"
|
|
347
|
+
echo " the CI step runs \`deploy --no-scheduler\`. Re-sync triggers yourself when a"
|
|
348
|
+
echo " job's cron: changes: $(cmd sync-triggers)"
|
|
258
349
|
fi
|
|
259
|
-
echo ""
|
|
260
|
-
echo " Inspect everything the agent can do:"
|
|
261
|
-
echo " gcloud projects get-iam-policy ${PROJECT} --flatten='bindings[].members' \\"
|
|
262
|
-
echo " --filter='bindings.members:${RUNTIME_SA}'"
|
|
263
350
|
echo "============================================================"
|