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
9 changes: 9 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ You are assisting in this repository. The following rules are **mandatory** and

## Testing & Validation

0. **Lint only with the pinned toolchain, memory-capped**

- `cla-backend-go` lint = golangci-lint **v1.64.8 built with and run under the CI Go toolchain** (`go 1.25.x`, `go.mod` / `build-pr.yml`).
Any other combination (newer Go, newer `GOTOOLCHAIN`) re-type-checks the module per linter, exceeds 30 GB RSS and OOM-kills the
whole session. Before linting: `GOTOOLCHAIN=go1.25.13 golangci-lint version` must report that Go version.
- Run it on Linux only, one at a time, with `--concurrency 4` (`make lint LINT_ARGS="--concurrency 4"`) inside a memory-capped cgroup
(`systemd-run --user --scope -p MemoryMax=16G -p MemorySwapMax=0 …`). Never replace a shared `~/go/bin/golangci-lint` while other
sessions run; install a pinned copy elsewhere and use `LINT_TOOL=<path>`.

5. **Always test changes**

- Whenever you modify code, **run tests** relevant to the change to ensure correctness.
Expand Down
251 changes: 251 additions & 0 deletions .github/workflows/org-import-sweep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
---
# Copyright The Linux Foundation and each contributor to CommunityBridge.
# SPDX-License-Identifier: MIT

# M3 organization import (lfx-self-serve #2750). Runbook: cla-backend-go/cmd/org_import/README.md.
# Manual runs choose stage/mode/routes. Scheduled runs are register-only and do nothing unless the
# repository variable ORG_IMPORT_SWEEP_DEV / ORG_IMPORT_SWEEP_PROD is set to dry-run or apply.
# Every run writes run.log + CSV reports (artifact), copies run.log to CloudWatch Logs
# /easycla/org-import/<stage> and e-mails the full decision record to SSM cla-org-import-report-emails-<stage>.

name: Org import sweep

on:
workflow_dispatch:
inputs:
stage:
description: Stage
type: choice
options: [dev, prod]
default: dev
mode:
description: dry-run writes nothing; apply performs the plan
type: choice
options: [dry-run, apply]
default: dry-run
routes:
description: Routes to process
type: choice
options: [register, rewrite, 'register,rewrite']
default: register
tranche:
description: Max groups to act on (0 = all)
type: string
default: '0'
ids:
description: Optional comma-separated external ids (old or new)
type: string
default: ''
mapping:
description: Optional mapping CSV for the rewrite route (rows separated by newlines or '|'), header old_id,new_id,action,approved
type: string
default: ''
decisions:
description: Optional duplicate-review decisions CSV (rows separated by newlines or '|'), header decision,old_ids,target_sfid,reviewer,note
type: string
default: ''
shared_domains:
description: Optional shared-domain list (one domain per line or '|'-separated); empty = built-in list
type: string
default: ''
notify:
description: E-mail the full run report to SSM cla-org-import-report-emails-<stage>
type: boolean
default: true
schedule:
- cron: '0 6 * * *'

permissions:
id-token: write
contents: read
actions: read

jobs:
plan:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- id: plan
env:
EVENT: ${{ github.event_name }}
IN_STAGE: ${{ inputs.stage }}
IN_MODE: ${{ inputs.mode }}
IN_ROUTES: ${{ inputs.routes }}
IN_TRANCHE: ${{ inputs.tranche }}
IN_IDS: ${{ inputs.ids }}
IN_NOTIFY: ${{ inputs.notify }}
SWEEP_DEV: ${{ vars.ORG_IMPORT_SWEEP_DEV }}
SWEEP_PROD: ${{ vars.ORG_IMPORT_SWEEP_PROD }}
run: |
set -euo pipefail
role() {
case "$1" in
dev) echo 'arn:aws:iam::395594542180:role/github-actions-deploy' ;;
prod) echo 'arn:aws:iam::716487311010:role/github-actions-deploy' ;;
*) echo "unknown stage $1" >&2; exit 1 ;;
esac
}
entry() {
jq -cn --arg stage "$1" --arg mode "$2" --arg routes "$3" --arg tranche "$4" --arg ids "$5" --arg notify "$6" --arg role "$(role "$1")" \
'{stage:$stage, mode:$mode, routes:$routes, tranche:$tranche, ids:$ids, notify:$notify, role:$role}'
}
entries=()
if [ "$EVENT" = workflow_dispatch ]; then
entries+=("$(entry "$IN_STAGE" "$IN_MODE" "$IN_ROUTES" "${IN_TRANCHE:-0}" "$IN_IDS" "${IN_NOTIFY:-true}")")
else
for pair in "dev:${SWEEP_DEV:-off}" "prod:${SWEEP_PROD:-off}"; do
stage="${pair%%:*}"; mode="${pair#*:}"
case "$mode" in
dry-run|apply) entries+=("$(entry "$stage" "$mode" register 0 '' true)") ;;
*) echo "scheduled sweep for $stage is off" ;;
esac
done
fi
matrix="$(printf '%s\n' "${entries[@]:-}" | jq -cs 'map(select(. != null))')"
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "$matrix"

sweep:
needs: plan
if: needs.plan.outputs.matrix != '[]'
runs-on: ubuntu-latest
timeout-minutes: 180
environment: ${{ matrix.stage }}
concurrency:
group: org-import-${{ matrix.stage }}
cancel-in-progress: false
strategy:
fail-fast: false
max-parallel: 1
matrix:
include: ${{ fromJSON(needs.plan.outputs.matrix) }}
env:
AWS_REGION: us-east-1
STAGE: ${{ matrix.stage }}
steps:
- uses: actions/checkout@v4
- name: Setup go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup python (swagger tooling)
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
cache-dependency-path: cla-backend-go/swagger/requirements.txt
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
audience: sts.amazonaws.com
role-to-assume: ${{ matrix.role }}
aws-region: us-east-1
- name: Cache Go modules
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-

- name: Configure Git to clone private Github repos
run: git config --global url."https://${TOKEN_USER}:${TOKEN}@github.com".insteadOf "https://github.com"
env:
TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_GITHUB }}
TOKEN_USER: ${{ secrets.PERSONAL_ACCESS_TOKEN_USER_GITHUB }}

- name: Add OS Tools
run: sudo apt update && sudo apt-get install file -y

- name: Go Setup
working-directory: cla-backend-go
run: make clean setup

- name: Go Dependencies
working-directory: cla-backend-go
run: make deps

- name: Go Swagger Generate
working-directory: cla-backend-go
run: make swagger

- name: Build org-import
working-directory: cla-backend-go
run: make build-org-import-linux

- name: Restore state and operator files
working-directory: cla-backend-go
env:
GH_TOKEN: ${{ github.token }}
MAPPING: ${{ github.event_name == 'workflow_dispatch' && inputs.mapping || '' }}
DECISIONS: ${{ github.event_name == 'workflow_dispatch' && inputs.decisions || '' }}
SHARED_DOMAINS: ${{ github.event_name == 'workflow_dispatch' && inputs.shared_domains || '' }}
run: |
set -euo pipefail
mkdir -p org-import-out
run_id="$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=org-import-state-${STAGE}&per_page=10" \
--jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | last | .workflow_run.id // empty')"
if [ -n "$run_id" ]; then
# a state artifact exists: failing to restore it must fail the job, never start from an empty journal
gh run download "$run_id" -n "org-import-state-${STAGE}" -D org-import-out
echo "restored state from run $run_id ($(wc -l < org-import-out/state.jsonl) journal lines)"
else
echo "no state artifact for ${STAGE}: starting with an empty journal"
fi
lines() { printf '%s\n' "$1" | tr '|' '\n' | sed '/^[[:space:]]*$/d'; }
if [ -n "$MAPPING" ]; then
lines "$MAPPING" > org-import-out/mapping.csv
echo "mapping rows: $(($(wc -l < org-import-out/mapping.csv) - 1))"
fi
if [ -n "$DECISIONS" ]; then
lines "$DECISIONS" > org-import-out/decisions.csv
echo "decision rows: $(($(wc -l < org-import-out/decisions.csv) - 1))"
fi
if [ -n "$SHARED_DOMAINS" ]; then
lines "$SHARED_DOMAINS" > org-import-out/shared_domains.txt
echo "shared domains: $(wc -l < org-import-out/shared_domains.txt)"
fi

- name: Run org-import ingest (${{ matrix.stage }} ${{ matrix.mode }} ${{ matrix.routes }})
working-directory: cla-backend-go
env:
MODE: ${{ matrix.mode }}
ROUTES: ${{ matrix.routes }}
TRANCHE: ${{ matrix.tranche }}
IDS: ${{ matrix.ids }}
NOTIFY: ${{ matrix.notify }}
run: |
set -euo pipefail
args=(ingest --routes "$ROUTES" --out-dir org-import-out --state org-import-out/state.jsonl)
[ "${TRANCHE:-0}" != "0" ] && args+=(--tranche "$TRANCHE")
[ -n "$IDS" ] && args+=(--ids "$IDS")
[ -s org-import-out/mapping.csv ] && args+=(--mapping org-import-out/mapping.csv)
[ -s org-import-out/decisions.csv ] && args+=(--decisions org-import-out/decisions.csv)
[ -s org-import-out/shared_domains.txt ] && args+=(--shared-domains org-import-out/shared_domains.txt)
[ "$NOTIFY" = false ] && args+=(--no-email)
[ "$MODE" = apply ] && args+=(--apply --yes)
echo "bin/org-import ${args[*]}"
bin/org-import "${args[@]}"

- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: org-import-out-${{ matrix.stage }}-${{ github.run_id }}
path: cla-backend-go/org-import-out
if-no-files-found: ignore
retention-days: 90

- name: Upload state
if: always() && matrix.mode == 'apply'
uses: actions/upload-artifact@v4
with:
name: org-import-state-${{ matrix.stage }}
path: cla-backend-go/org-import-out/state.jsonl
if-no-files-found: ignore
retention-days: 90
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,4 @@ spans*.json
.venv
copilot-*.md
/e2e-tests/
org-import-out/
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@ make setup # one-time: install swagger, golangci-lint, goimports; sets
make swagger # regenerate API models/clients from swagger specs into gen/ (see below)
make build-mac # build local binary -> bin/cla-mac (build-linux for Linux)
make test # go test -v ./... with coverage
make lint # golangci-lint (v1.64.8, config .golangci.yaml) + license header check
make lint # golangci-lint (v1.64.8, config .golangci.yaml) + license header check — see "Lint safely" below
make fmt # gofmt + goimports
make mock # regenerate mocks via tools/regenmocks.sh
make all-mac # full pipeline: clean swagger deps fmt build test lint (all-linux on Linux)
```

Run a single test: `go test -v ./signatures/ -run TestName`

### Lint safely (memory)

golangci-lint v1.64.8 must be the binary built with the CI Go toolchain (`go 1.25.x`, see `go.mod`/`build-pr.yml`) and run under that
toolchain. A v1.64.8 binary built with a newer Go, or run with a newer `GOTOOLCHAIN`, type-checks every package again per linter and grows
past 30 GB RSS on this module — it OOM-kills the whole shell/tmux session, not just the linter. Rules: Linux only; pinned toolchain
(`GOTOOLCHAIN=go1.25.13 golangci-lint version` must print the same Go version); `LINT_ARGS="--concurrency 4"` on shared machines; run it in
its own memory-capped cgroup (`systemd-run --user --scope -p MemoryMax=16G -p MemorySwapMax=0 make lint`); one lint at a time; never overwrite
a shared `~/go/bin/golangci-lint` while other sessions may use it — install a pinned copy elsewhere and point `LINT_TOOL` at it.

Run locally (points at a real AWS environment — see below): build, set env, then `./bin/cla-mac` (from `make build-mac`) or `./bin/cla` (from `make build-linux`). Health checks at `http://localhost:8080/v3/ops/health` and `/v4/ops/health`. Set `GH_ORG_VALIDATION=false` to bypass GitHub auth checks for local curl/Postman testing.

### Swagger code generation (important)
Expand Down
16 changes: 15 additions & 1 deletion cla-backend-go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ GITLAB_REPO_CHECK_BIN = gitlab-repository-check-lambda
FUNCTIONAL_TESTS_BIN = functional-tests
USER_SUBSCRIBE_BIN = user-subscribe-lambda
REPOSITORY_UPDATE_BIN = repository-update-tool
ORG_IMPORT_BIN = org-import
MAKEFILE_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
GOPRIVATE=github.com/LF-Engineering/*
BUILD_TIME=$(shell sh -c 'date -u +%FT%T%z')
Expand All @@ -35,6 +36,8 @@ ifeq "$(shell uname -s)" "Linux"
endif

LINT_TOOL=$(shell go env GOPATH)/bin/golangci-lint
# extra golangci-lint flags, e.g. LINT_ARGS="--concurrency 4" on shared/low-memory machines
LINT_ARGS ?=
# LINT_VERSION=v1.51.2
LINT_VERSION=v1.64.8
SWAGGER_DIR=$(ROOT_DIR)/swagger
Expand Down Expand Up @@ -328,6 +331,17 @@ build-gitlab-repository-check-lambda-mac: deps build-prep
env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(GITLAB_REPO_CHECK_BIN)-mac cmd/gitlab_repository_check/main.go
@chmod +x $(BIN_DIR)/$(GITLAB_REPO_CHECK_BIN)-mac

build-org-import: build-org-import-linux
build-org-import-linux: deps build-prep
@echo "==> Building the org import tool (Linux amd64)..."
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BIN_DIR)/$(ORG_IMPORT_BIN) cmd/org_import/main.go
@chmod +x $(BIN_DIR)/$(ORG_IMPORT_BIN)

build-org-import-mac: deps build-prep
@echo "==> Building the org import tool (Mac OSX)..."
env CGO_ENABLED=0 GOOS=darwin GOARCH=$(BUILD_ARCH) go build $(LDFLAGS) -o $(BIN_DIR)/$(ORG_IMPORT_BIN)-mac cmd/org_import/main.go
@chmod +x $(BIN_DIR)/$(ORG_IMPORT_BIN)-mac

build-functional-tests: build-functional-tests-linux
build-functional-tests-linux: deps build-prep
@echo "==> Building Functional Tests for Linux amd64 binary..."
Expand All @@ -351,5 +365,5 @@ build-repository-update-mac: deps build-prep
@chmod +x $(BIN_DIR)/$(REPOSITORY_UPDATE_BIN)-mac

lint:
@cd $(MAKEFILE_DIR) && $(LINT_TOOL) version && echo "==> Running lint..." && $(LINT_TOOL) run --timeout 30m --exclude="this method will not auto-escape HTML. Verify data is well formed" --allow-parallel-runners --config=.golangci.yaml ./... && echo "==> Lint check passed."
@cd $(MAKEFILE_DIR) && $(LINT_TOOL) version && echo "==> Running lint..." && $(LINT_TOOL) run --timeout 30m --exclude="this method will not auto-escape HTML. Verify data is well formed" --allow-parallel-runners --config=.golangci.yaml $(LINT_ARGS) ./... && echo "==> Lint check passed."
@cd $(MAKEFILE_DIR) && ./check-headers.sh
Loading
Loading