Skip to content
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
8 changes: 4 additions & 4 deletions .github/workflows/cd-apply-kuma-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,21 @@ jobs:
# must not apply unmerged kuma-config to prod.
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3.0.0
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: ${{ vars.GCP_WIF }}
service_account: ${{ vars.GCP_KUMA_APPLIER_SA }}
project_id: ${{ vars.GCP_PROJECT }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v3.0.1
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1

- uses: actions/setup-node@v7.0.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '20'

Expand Down
20 changes: 12 additions & 8 deletions .github/workflows/chore-clean-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,30 @@ jobs:
id-token: 'write'
steps:
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v5.7.0
uses: rlespinasse/github-slug-action@ef93b2ea4b6405d06fd8684fc3ff795d262ecae8 # v5.7.0

- name: Authenticate to Google Cloud
id: auth
uses: google-github-actions/auth@v3.0.0
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: '${{ vars.GCP_WIF }}'
project_id: '${{ vars.GCP_PROJECT }}'

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v3.0.1
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1

- name: Removing CR service
# Expressions go through env (not inline in run) so shell never parses
# attacker-influenced text — see zizmor's template-injection audit.
env:
SVC: ${{ vars.APP_NAME }}-${{ env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
REGION: ${{ vars.GCP_REGION }}
run: |
svc="${{ vars.APP_NAME }}-${{ env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}"
# Only branches that deployed a preview Kuma have a service; skip the rest
# (config/dependabot PRs, branch deletes) instead of failing on "not found".
if gcloud run services describe "$svc" --region="${{ vars.GCP_REGION }}" >/dev/null 2>&1; then
gcloud run services delete "$svc" --region="${{ vars.GCP_REGION }}" --quiet
echo "deleted $svc"
if gcloud run services describe "$SVC" --region="$REGION" >/dev/null 2>&1; then
gcloud run services delete "$SVC" --region="$REGION" --quiet
echo "deleted $SVC"
else
echo "no dev service '$svc' — nothing to clean"
echo "no dev service '$SVC' — nothing to clean"
fi
17 changes: 15 additions & 2 deletions .github/workflows/ci-lint-codebase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code Repository
uses: actions/checkout@v7.0.1
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Full git history is needed to get a proper
# list of changed files within `super-linter`
fetch-depth: 0
# Lint only reads the tree; don't leave the token on disk (zizmor artipacked).
persist-credentials: false

- name: Lint Code Base
uses: super-linter/super-linter/slim@v8.7.0
uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0
env:
LOG_LEVEL: ERROR
# Job summary already reports per-linter results; posting commit
# statuses needs `statuses: write`, which this job doesn't grant.
MULTI_STATUS: false
VALIDATE_ALL_CODEBASE: false
VALIDATE_SHELL_SHFMT: false
VALIDATE_JSCPD: false
Expand All @@ -52,6 +57,14 @@ jobs:
VALIDATE_JAVASCRIPT_ES: false
VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_DOCKERFILE_HADOLINT: false
# One formatter per language: Biome owns JS/JSON (biome.json at repo
# root), yamllint (VALIDATE_YAML) owns YAML. Running Prettier on the
# same files fights Biome's style and double-reports. Markdown lint
# is off (VALIDATE_MARKDOWN above), so its formatter is off too.
VALIDATE_JAVASCRIPT_PRETTIER: false
VALIDATE_JSON_PRETTIER: false
VALIDATE_YAML_PRETTIER: false
VALIDATE_MARKDOWN_PRETTIER: false
LINTER_RULES_PATH: /
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14 changes: 7 additions & 7 deletions .github/workflows/sub-build-docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v5.7.0
uses: rlespinasse/github-slug-action@ef93b2ea4b6405d06fd8684fc3ff795d262ecae8 # v5.7.0
with:
short-length: 7

# Automatic tag management and OCI Image Format Specification for labels
- name: Docker meta
id: meta
uses: docker/metadata-action@v6.2.0
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
# list of Docker images to use as base name for tags
images: |
Expand All @@ -76,11 +76,11 @@ jobs:
# Setup Docker Buildx to allow use of docker cache layers from GH
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v4.2.0
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Authenticate to Google Cloud
id: auth
uses: google-github-actions/auth@v3.0.0
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: '${{ vars.GCP_WIF }}'
service_account: '${{ vars.GCP_ARTIFACTS_SA }}'
Expand All @@ -91,7 +91,7 @@ jobs:
access_token_lifetime: 10800s

- name: Login to Google Artifact Registry
uses: docker/login-action@v4.5.1
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with:
registry: us-docker.pkg.dev
username: oauth2accesstoken
Expand All @@ -100,7 +100,7 @@ jobs:
# Build and push image to Google Artifact Registry, and possibly DockerHub
- name: Build & push
id: docker_build
uses: docker/build-push-action@v7.3.0
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
target: ${{ inputs.dockerfile_target }}
context: .
Expand Down
31 changes: 20 additions & 11 deletions .github/workflows/sub-cloudrun-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,17 @@ jobs:
steps:
- name: Getting API Version
id: get
uses: actions/github-script@v9
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
if: ${{ github.event_name == 'release' }}
with:
result-encoding: string
script: |
return context.payload.release.tag_name.substring(0,2)
- name: Setting API Version
id: set
run: echo "version=${{ steps.get.outputs.result }}" >> "$GITHUB_OUTPUT"
env:
VERSION: ${{ steps.get.outputs.result }}
run: echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

deploy:
name: Deploy to Cloud Run
Expand All @@ -99,15 +101,15 @@ jobs:
id-token: write
steps:
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v5.7.0
uses: rlespinasse/github-slug-action@ef93b2ea4b6405d06fd8684fc3ff795d262ecae8 # v5.7.0

- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Authenticate to Google Cloud
id: auth
uses: google-github-actions/auth@v3.0.0
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: '${{ vars.GCP_WIF }}'
project_id: '${{ vars.GCP_PROJECT }}'
Expand All @@ -117,19 +119,21 @@ jobs:
service_account: '${{ vars.GCP_DEPLOYMENTS_SA }}'

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v3.0.1
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1

# Restricted ingress closes the *.run.app URL, so we need public_url for the
# Environment URL and smoke test — fail fast instead of silently skipping it.
- name: Require public_url for restricted ingress
if: ${{ inputs.ingress != 'all' && inputs.public_url == '' }}
env:
INGRESS: ${{ inputs.ingress }}
run: |
echo "::error::ingress=${{ inputs.ingress }} requires public_url (the *.run.app URL is closed)"
echo "::error::ingress=${INGRESS} requires public_url (the *.run.app URL is closed)"
exit 1

- name: Deploy to cloud run
id: deploy
uses: google-github-actions/deploy-cloudrun@v3.0.1
uses: google-github-actions/deploy-cloudrun@2028e2d7d30a78c6910e0632e48dd561b064884d # v3.0.1
with:
service: ${{ inputs.app_name }}-${{ needs.versioning.outputs.version || env.GITHUB_HEAD_REF_SLUG || inputs.environment }}
image: ${{ inputs.registry }}/${{ inputs.app_name }}@${{ inputs.image_digest }}
Expand Down Expand Up @@ -160,11 +164,16 @@ jobs:

# Required even with internal ingress: the LB invokes Cloud Run unauthenticated.
- name: Allow unauthenticated calls to the service
env:
SERVICE: ${{ inputs.app_name }}-${{ needs.versioning.outputs.version || env.GITHUB_HEAD_REF_SLUG || inputs.environment }}
REGION: ${{ inputs.region }}
run: |
gcloud run services add-iam-policy-binding ${{ inputs.app_name }}-${{ needs.versioning.outputs.version || env.GITHUB_HEAD_REF_SLUG || inputs.environment }} \
--region=${{ inputs.region }} --member=allUsers --role=roles/run.invoker --quiet
gcloud run services add-iam-policy-binding "$SERVICE" \
--region="$REGION" --member=allUsers --role=roles/run.invoker --quiet

# Curl the reachable URL: the direct one for `all`, else the public LB URL
# (guaranteed present by the guard above).
- name: Test service with cURL
run: curl --retry 5 --retry-all-errors --max-time 30 -fsSL "${{ inputs.ingress == 'all' && steps.deploy.outputs.url || inputs.public_url }}" -o /dev/null
env:
TARGET_URL: ${{ inputs.ingress == 'all' && steps.deploy.outputs.url || inputs.public_url }}
run: curl --retry 5 --retry-all-errors --max-time 30 -fsSL "$TARGET_URL" -o /dev/null
12 changes: 12 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
"formatter": {
"indentStyle": "space",
"indentWidth": 4
},
"json": {
"formatter": {
"indentWidth": 2
}
}
}
9 changes: 5 additions & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ ENV UPTIME_KUMA_IS_CONTAINER=1

EXPOSE ${PORT}

# The upstream uptime-kuma image is designed to run as root: it manages its own
# data directory and child processes, and a non-root USER breaks startup. We
# inherit that here intentionally, so skip the non-root-user policy.
# checkov:skip=CKV_DOCKER_3:upstream uptime-kuma image runs as root by design
# Run as the image's built-in node user (uid 1000). Upstream pre-chowns
# /app/data to node, and 2.3.2 starts clean as non-root (verified: HTTP up,
# extra/healthcheck OK, DB written). Cloud Run's in-memory volume is a tmpfs
# writable by any uid, so the /app/data mount keeps working.
USER node

# Reuse Uptime Kuma's built-in healthcheck (same as the upstream image).
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 \
Expand Down
8 changes: 5 additions & 3 deletions kuma-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ big one — configuring **notifications declaratively** (prod currently has none
It's a **reconciler**, not strictly one-time: edit `kuma.yaml` and re-run when the
monitor/notification set changes. In practice those changes are infrequent and
run **locally/manually** by a team member, so secrets are passed as environment
variables at run time (we keep them in 1Password) — there's no managed secret to
variables at runtime (we keep them in 1Password) — there's no managed secret to
provision for this. The only Secret Manager entry related to Kuma is
`UPTIME_KUMA_DB_PASSWORD`, which the *runtime* needs (separate concern).

Requires Node.js 18+ (declared in `package.json` `engines`; CI uses 20).

```sh
npm install

Expand Down Expand Up @@ -57,7 +59,7 @@ node apply.js # apply for real

`kuma.yaml` references a notification's secret by **env var name**
(`webhookEnv: SLACK_WEBHOOK_URL`) — the value is injected at apply time only and
never written to git.
never written to Git.

## Pipeline (prod)

Expand All @@ -69,7 +71,7 @@ tab (`workflow_dispatch`).
**No secrets live in GitHub.** The job authenticates with keyless **Workload
Identity Federation** as a least-privilege service account (`kuma-config-applier`,
which holds only `secretAccessor` on the two secrets it reads) and pulls the admin
password and Slack webhook from **Secret Manager** at run time — one source of
password and Slack webhook from **Secret Manager** at runtime — one source of
truth, one rotation point.

To operate it, the `prod` GitHub environment provides these non-secret
Expand Down
Loading