diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..d2e3510 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,5 @@ +FROM schmalz.jfrog.io/default-docker/db-devcontainer-base:1 + +# ===== ===== ===== ===== ===== ===== +# Project specific setup +# ===== ===== ===== ===== ===== ===== diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..fdde937 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "build": { + "dockerfile": "Dockerfile" + }, + "mounts": [ + "source=${localEnv:HOME}/.devcontainer/.config/,target=/home/ubuntu/.config/,type=bind,consistency=cached", + "source=${localEnv:HOME}/.devcontainer/.zshrc,target=/home/ubuntu/.zshrc_user,type=bind,consistency=cached", + "source=${localEnv:HOME}/.devcontainer/.commandhistory/,target=/commandhistory/,type=bind,consistency=cached", + "source=${localEnv:HOME}/.devcontainer/.aws/,target=/home/ubuntu/.aws/,type=bind,consistency=cached" + // Project specific mounts + ], + "customizations": { + "vscode": { + "extensions": [ + "amazonwebservices.aws-toolkit-vscode" + ] + } + } +} diff --git a/.forgejo/workflows/full-scan-aikido.yml b/.forgejo/workflows/full-scan-aikido.yml new file mode 100644 index 0000000..494ad60 --- /dev/null +++ b/.forgejo/workflows/full-scan-aikido.yml @@ -0,0 +1,19 @@ +name: Aikido Security Full Scan + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + aikido-full-scan: + name: Aikido Security Full Scan + runs-on: stackit-ubuntu-22 + steps: + - name: Checkout repository + uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/checkout@checkout-v1 + + - name: Run Aikido full scan + uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/aikido-full-scan@aikido-full-scan-v1 + with: + apikey: ${{ secrets.AIKIDO_CLIENT_API_KEY }} diff --git a/.forgejo/workflows/pr-check-aikido.yml b/.forgejo/workflows/pr-check-aikido.yml new file mode 100644 index 0000000..1687d9c --- /dev/null +++ b/.forgejo/workflows/pr-check-aikido.yml @@ -0,0 +1,21 @@ +name: Aikido Security PR Check + +on: + pull_request: + +concurrency: + group: ${{ forgejo.workflow }}-${{ forgejo.ref }} + cancel-in-progress: true + +jobs: + aikido-security: + name: Aikido Security Scan + runs-on: stackit-ubuntu-22 + steps: + - name: Checkout repository + uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/checkout@checkout-v1 + + - name: Security scan + uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/aikido-pr-scan@aikido-pr-scan-v1 + with: + apikey: ${{ secrets.AIKIDO_CLIENT_API_KEY }} diff --git a/.forgejo/workflows/pr-check-shared-actions.yml b/.forgejo/workflows/pr-check-shared-actions.yml new file mode 100644 index 0000000..25d68e7 --- /dev/null +++ b/.forgejo/workflows/pr-check-shared-actions.yml @@ -0,0 +1,20 @@ +name: validate-shared-actions + +on: + pull_request: + +permissions: + contents: read + +jobs: + validate-shared-actions: + runs-on: stackit-ubuntu-22 + steps: + - name: Checkout + uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/checkout@checkout-v1 + + - name: Validate shared action metadata + uses: docker://data.forgejo.org/forgejo/runner:12 + with: + entrypoint: /bin/sh + args: -ec "find . -mindepth 2 -maxdepth 2 -name action.yml -exec forgejo-runner validate --action --path {} \\;" diff --git a/.forgejo/workflows/tag-release.yml b/.forgejo/workflows/tag-release.yml new file mode 100644 index 0000000..14a2b68 --- /dev/null +++ b/.forgejo/workflows/tag-release.yml @@ -0,0 +1,92 @@ +name: tag-release + +# Manually create or move a major release tag for a shared action. +# Tag format: -v (e.g. checkout-v1, pnpm-build-v2) +# +# If the tag already exists it is force-moved to the selected ref. + +on: + workflow_dispatch: + inputs: + action: + description: Action to release + required: true + type: choice + options: + - aikido-full-scan + - aikido-pr-scan + - aws-configure + - aws-lambda-alias-update + - aws-lambda-wait-for-provisioned-concurrency + - cache + - checkout + - download-artifact + - esb-deploy + - helm-deploy + - i18n-sync + - inject-content + - maven-build + - playwright-merge + - playwright-run + - pnpm-build + - publish-npm-package + - publish-rust-crate + - publish-static-contents + - rust-build + - terraform-apply + - terraform-plan + - terraform-validate + - upload-artifact + - vacuum-lint + major-version: + description: 'Major version number (e.g. 1)' + required: true + type: string + ref: + description: 'Branch, tag, or commit SHA to tag (defaults to the default branch)' + required: false + type: string + default: '' + +permissions: + contents: write + +jobs: + tag-release: + runs-on: stackit-ubuntu-22 + steps: + - name: Checkout + uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/checkout@checkout-v1 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + + - name: Validate major version + env: + MAJOR: ${{ inputs.major-version }} + run: | + if ! echo "$MAJOR" | grep -qE '^[0-9]+$'; then + echo "::error::major-version must be a positive integer, got: $MAJOR" + exit 1 + fi + + - name: Create or move major tag + env: + ACTION: ${{ inputs.action }} + MAJOR: ${{ inputs.major-version }} + run: | + set -euo pipefail + + TAG="${ACTION}-v${MAJOR}" + COMMIT=$(git rev-parse HEAD) + + echo "Tag : $TAG" + echo "Commit : $COMMIT" + + git config user.name "forgejo-actions[bot]" + git config user.email "forgejo-actions[bot]@noreply.forgejo.org" + + git tag -f "$TAG" "$COMMIT" + git push origin -f "refs/tags/$TAG" + + echo "::notice::Tag $TAG now points to $COMMIT" diff --git a/.forgejo/workflows/validate-shared-actions.yml b/.forgejo/workflows/validate-shared-actions.yml deleted file mode 100644 index 570918d..0000000 --- a/.forgejo/workflows/validate-shared-actions.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: validate-shared-actions - -on: - pull_request: - types: [opened, reopened, synchronize] - -permissions: - contents: read - -jobs: - validate-shared-actions: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Validate shared action metadata - uses: mpalmer/action-validator@v0.9.0 - with: - version: 0.9.0 - patterns: | - :(glob)**/action.yml - :(glob)**/action.yaml diff --git a/2026-04-17-forgejo-shared-actions-migration.md b/2026-04-17-forgejo-shared-actions-migration.md deleted file mode 100644 index 6837c71..0000000 --- a/2026-04-17-forgejo-shared-actions-migration.md +++ /dev/null @@ -1,1280 +0,0 @@ -# Forgejo Shared Actions Migration Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Migrate all Bitbucket + Jenkins CI/CD pipelines to STACKIT Git (Forgejo Actions) using a shared-actions repository with one reusable composite action per responsibility. - -**Architecture:** A single `schmalz/shared-actions` repository contains all reusable composite actions and reusable workflows. Consumer repositories call them via `uses: schmalz/shared-actions/.github/actions/@v1`. Each action has exactly one responsibility and is fully self-contained. AWS authentication moves from EC2InstanceMetadata to OIDC federation. - -> **STACKIT Git URL:** `https://schmalz-git.git.onstackit.cloud` (DNS may move to `git.schmalzgroup.com` — update all `uses:` references and OIDC trust policies when it does). - -**Tech Stack:** Forgejo Actions (GitHub Actions-compatible YAML), OIDC AWS auth, pnpm, Maven/Jib, Terraform, Playwright, Rust/Cargo, JFrog Artifactory, Helm/Kubernetes - ---- - -## Context & Background - -### Source Systems Being Replaced - -| System | Count | Details | -|--------|-------|---------| -| Jenkins pipelines | 18 | Java/Maven, Helm deploy to on-prem Kubernetes | -| Bitbucket pipelines | ~53 | TypeScript Lambda, Terraform, Playwright, Rust | -| Custom Bitbucket Pipes | 2 | Docker-based: SBOM upload, i18n translation sync | - -### Key Design Decisions - -| Decision | Choice | Reason | -|----------|--------|--------| -| Shared repo | `schmalz/shared-actions` (new repo) | Clean separation, versioned independently | -| Version pinning | Semver tags `@v1` / `@v1.2.3` | Stability with explicit upgrade path | -| Runners | STACKIT-hosted runners | No self-hosted infrastructure to maintain | -| AWS auth | OIDC federation | EC2InstanceMetadata not available on STACKIT runners | -| Custom pipes | Convert to composite actions (scripts) | No Docker overhead, easier to maintain | -| K8s target (Jenkins) | On-prem — services stay on `dsp1.schmalzgroup.net` / `dsp1-stage.schmalzgroup.net`; `helm-deploy` is permanent | | -| Third-party actions | Use fully qualified `https://data.forgejo.org/` URLs | Avoids `DEFAULT_ACTIONS_URL` misconfiguration on the STACKIT instance | - -### AWS Accounts (preserved from existing pipelines) - -| Environment | Account ID | IAM Role | -|-------------|-----------|----------| -| Stage | `953815822701` | `arn:aws:iam::953815822701:role/deployment-role` | -| Prod | `409901728745` | `arn:aws:iam::409901728745:role/deployment-role` | -| Dev | `916033256208` | `arn:aws:iam::916033256208:role/deployment-role` | - -### JFrog Artifactory (preserved) - -- npm registry: `https://schmalz.jfrog.io/artifactory/api/npm/default-npm/` -- Docker registry: `schmalz.jfrog.io/default-docker` -- Terraform modules: `https://schmalz.jfrog.io/artifactory/terraform/` -- Cargo registry: `schmalz.jfrog.io` -- Auth: `JFROG_TOKEN` secret (set per-repo or at org level) - ---- - -## Repository Structure: `schmalz/shared-actions` - -``` -schmalz/shared-actions/ -├── .github/ -│ ├── actions/ -│ │ ├── aws-configure/ -│ │ │ └── action.yml -│ │ ├── maven-build/ -│ │ │ └── action.yml -│ │ ├── helm-deploy/ -│ │ │ └── action.yml -│ │ ├── secrets-inject/ -│ │ │ └── action.yml -│ │ ├── pnpm-build/ -│ │ │ └── action.yml -│ │ ├── terraform-validate/ -│ │ │ └── action.yml -│ │ ├── terraform-apply/ -│ │ │ └── action.yml -│ │ ├── aws-s3-sync/ -│ │ │ └── action.yml -│ │ ├── aws-lambda-update/ -│ │ │ └── action.yml -│ │ ├── playwright-e2e/ -│ │ │ └── action.yml -│ │ ├── rust-build/ -│ │ │ └── action.yml -│ │ ├── publish-npm-package/ -│ │ │ └── action.yml -│ │ ├── sbom-upload/ -│ │ │ └── action.yml -│ │ ├── terraform-module-publish/ -│ │ │ └── action.yml -│ │ ├── docker-build-push/ -│ │ │ └── action.yml -│ │ ├── publish-rust-crate/ -│ │ │ └── action.yml -│ │ └── cloudfront-invalidate/ -│ │ └── action.yml -│ └── workflows/ -│ └── (reusable workflows if needed later) -└── README.md -``` - ---- - -## One-Time Infrastructure Setup (Before Any Migration) - -### OIDC Setup in AWS (required — blocks all migration work) - -1. In AWS IAM console for **both** accounts (stage `953815822701`, prod `409901728745`): - - Add an OIDC Identity Provider for the STACKIT Git instance - - URL: `https://schmalz-git.git.onstackit.cloud` - - Audience: `sts.amazonaws.com` -2. Update the trust policy on `deployment-role` in each account to allow the OIDC provider: - ```json - { - "Effect": "Allow", - "Principal": { - "Federated": "arn:aws:iam:::oidc-provider/schmalz-git.git.onstackit.cloud" - }, - "Action": "sts:AssumeRoleWithWebIdentity", - "Condition": { - "StringLike": { - "schmalz-git.git.onstackit.cloud:sub": "repo:schmalz/*:*" - }, - "StringEquals": { - "schmalz-git.git.onstackit.cloud:aud": "sts.amazonaws.com" - } - } - } - ``` - > **Note:** The Forgejo OIDC `iss` claim is `https://schmalz-git.git.onstackit.cloud/api/actions`. AWS uses the hostname only (without `/api/actions`) as the IdP identifier. - > - > If the DNS moves to `git.schmalzgroup.com`, a new OIDC provider entry must be added in AWS and both `sub` + `aud` conditions must be updated. -3. Set `JFROG_TOKEN` as an organization-level secret in STACKIT Git (avoid per-repo duplication). - -### Third-Party Action URLs (required — must be confirmed before Phase 1) - -Forgejo resolves bare action references like `actions/checkout@v4` by prepending `DEFAULT_ACTIONS_URL`, which defaults to `https://data.forgejo.org/` but can be overridden by the instance admin. - -**Action:** Confirm with the STACKIT instance admin what `DEFAULT_ACTIONS_URL` is set to on the managed instance. - -Until confirmed, all consumer workflows use **fully qualified URLs**: - -| Action | Qualified URL | -|--------|---------------| -| `actions/checkout@v4` | `https://data.forgejo.org/actions/checkout@v4` | -| `actions/upload-artifact@v4` | `https://data.forgejo.org/actions/upload-artifact@v4` | -| `actions/download-artifact@v4` | `https://data.forgejo.org/actions/download-artifact@v4` | - -The `aws-configure` action does **not** depend on `aws-actions/configure-aws-credentials`. It performs the OIDC token exchange directly via shell (see action spec below) — no external action required. - -### `enable-openid-connect` (required for all AWS workflows) - -Forgejo uses `enable-openid-connect: true` at the workflow or job level to enable OIDC token generation. This is **not** the same as GitHub's `permissions: id-token: write`. Every workflow or job that calls `aws-configure` must include this flag. - ---- - -## Shared Action Specifications - -### Action 1: `aws-configure` - -**Responsibility:** Authenticate with AWS via OIDC. Called before any step that needs AWS CLI. Replaces all `aws configure --profile ... set credential_source Ec2InstanceMetadata` blocks. - -**Inputs:** - -| Input | Required | Default | Description | -|-------|----------|---------|-------------| -| `role-arn` | yes | — | Full IAM role ARN | -| `aws-profile` | no | `default` | Profile name written to `~/.aws/config` | -| `region` | no | `eu-central-1` | AWS region | - -**What it does** (pure shell, no external action dependency): -```bash -# Exchange Forgejo OIDC token for AWS credentials -OIDC_TOKEN=$(curl -sf \ - -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ - "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value) - -CREDS=$(aws sts assume-role-with-web-identity \ - --role-arn "$INPUT_ROLE_ARN" \ - --role-session-name forgejo-ci \ - --web-identity-token "$OIDC_TOKEN" \ - --region "$INPUT_REGION" \ - --query 'Credentials' --output json) - -mkdir -p ~/.aws - -# Write environment credentials (used by AWS CLI/SDK by default) -echo "AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r .AccessKeyId)" >> $FORGEJO_ENV -echo "AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r .SecretAccessKey)" >> $FORGEJO_ENV -echo "AWS_SESSION_TOKEN=$(echo $CREDS | jq -r .SessionToken)" >> $FORGEJO_ENV -echo "AWS_DEFAULT_REGION=$INPUT_REGION" >> $FORGEJO_ENV - -# Write named profile if requested -if [ "$INPUT_AWS_PROFILE" != "default" ]; then - aws configure set aws_access_key_id "$(echo $CREDS | jq -r .AccessKeyId)" --profile "$INPUT_AWS_PROFILE" - aws configure set aws_secret_access_key "$(echo $CREDS | jq -r .SecretAccessKey)" --profile "$INPUT_AWS_PROFILE" - aws configure set aws_session_token "$(echo $CREDS | jq -r .SessionToken)" --profile "$INPUT_AWS_PROFILE" - aws configure set region "$INPUT_REGION" --profile "$INPUT_AWS_PROFILE" -fi -``` - -**Requires:** `enable-openid-connect: true` at the job or workflow level in the calling workflow (Forgejo-specific; injects `ACTIONS_ID_TOKEN_REQUEST_TOKEN` and `ACTIONS_ID_TOKEN_REQUEST_URL` into the runner environment). - ---- - -### Action 2: `maven-build` - -**Responsibility:** Run Maven build for Java services — either verify-only (for PRs) or full package+jib push (for deploy branches). Replaces the duplicated Maven steps in all 18 Jenkinsfiles. - -**Inputs:** - -| Input | Required | Default | Description | -|-------|----------|---------|-------------| -| `java-version` | no | `8` | JDK version (8, 17, 25) | -| `maven-image` | no | `maven:3.8.5-openjdk-8` | Full Docker image for the build | -| `phase` | no | `verify` | `verify` (PR) or `package-and-push` (deploy) | -| `verify-goals` | no | `spotless:check checkstyle:check compile` | Maven goals for `verify` phase. Override per service if needed (e.g., `employee` uses `spotless:check checkstyle:check test`) | -| `maven-profile` | no | `test` | Maven `-P` profile (test, prod) | -| `service-dir` | no | `.` | Subdirectory containing the service pom.xml (for ESB multi-service) | -| `skip-tests` | no | `false` | Pass `-DskipTests` | -| `image-tag` | no | `${{ github.sha }}-${{ github.run_id }}` | Docker image tag for jib push | -| `maven-settings` | yes | — | Secret name containing `settings.xml` content | -| `extra-args` | no | `` | Additional Maven arguments | - -**Phase `verify` runs** (PR validation — matches existing Jenkins behavior): -``` -mvn -s -``` -> **Note:** The default `verify-goals` is `spotless:check checkstyle:check compile`, matching the current Jenkins PR behavior. This is intentionally lighter than `mvn verify` (which runs the full lifecycle including tests). Only `employee` overrides this to include `test`. - -**Phase `package-and-push` runs:** -``` -mvn clean package jib:build -DsendCredentialsOverHttp=true \ - -Djib.to.tags= -P -s -``` - -**Notes:** -- `maven-image` input allows overriding the full image (some services use `maven:3.9.12-eclipse-temurin-25` for Java 25) -- The action writes `settings.xml` from the secret to a temp file, passes it via `-s`, and deletes it after - ---- - -### Action 3: `helm-deploy` - -**Responsibility:** Deploy a service to Kubernetes via Helm over SSH. Replaces the `deployService()` Groovy function duplicated in every Jenkinsfile. - -**Inputs:** - -| Input | Required | Default | Description | -|-------|----------|---------|-------------| -| `service-name` | yes | — | Helm release name | -| `helm-host` | yes | — | SSH target (e.g., `dsp1-stage.schmalzgroup.net`) | -| `overrides-file` | no | `kubernetes/overrides-su.yaml` | Local path to Helm values override file | -| `image-tag` | yes | — | Docker image tag to deploy | -| `ssh-key` | yes | — | Secret name containing private SSH key | -| `namespace` | no | `dsp` | Kubernetes namespace | -| `helm-repo` | no | `nexus-helm-repository` | Helm chart repository name | -| `helm-chart` | no | `DSP-Blueprint` | Chart name in the repo | - -**What it does:** -```bash -# SCP overrides file to remote host -scp -i -o StrictHostKeyChecking=no root@:/tmp/-overrides.yaml -# Helm upgrade -ssh -i -o StrictHostKeyChecking=no root@ \ - "helm repo update && \ - helm upgrade --install --create-namespace -n \ - / -f /tmp/-overrides.yaml \ - --set image.tag= --atomic --debug" -``` - ---- - -### Action 4: `secrets-inject` - -**Responsibility:** Append a secrets file (from CI secret store) to a Java `.properties` file. Replaces the "Load secrets" stage in every Jenkinsfile. - -**Inputs:** - -| Input | Required | Default | Description | -|-------|----------|---------|-------------| -| `secret-content` | yes | — | Secret value (the properties content to append) | -| `target-file` | yes | — | Path to the `.properties` file (e.g., `src/main/resources/application-su.properties`) | - -**What it does:** -```bash -echo "" >> -echo "" >> -``` - -**Note:** The secret is passed as a value (not a file path) to work naturally with Forgejo Secrets. - ---- - -### Action 5: `pnpm-build` - -**Responsibility:** Set up pnpm, authenticate JFrog npm registry, install dependencies, run configurable npm scripts. Replaces the near-identical pnpm blocks in all ~50 Bitbucket TypeScript pipelines. - -**Inputs:** - -| Input | Required | Default | Description | -|-------|----------|---------|-------------| -| `working-directory` | no | `.` | Directory containing `package.json` | -| `pnpm-version` | no | `10.14` | pnpm version to install globally | -| `jfrog-token` | yes | — | JFrog npm auth token | -| `run-scripts` | no | `ci,typecheck,build` | Comma-separated list of `pnpm run