Skip to content

Commit e99bb5d

Browse files
authored
Merge pull request #4 from linuxfoundation/build-and-release
Add CI for building and releasing v1-sync-helper with ko
2 parents 093c1d1 + 218a85c commit e99bb5d

File tree

9 files changed

+192
-27
lines changed

9 files changed

+192
-27
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright The Linux Foundation and each contributor to LFX.
2+
# SPDX-License-Identifier: MIT
3+
---
4+
name: Publish Main
5+
6+
"on":
7+
push:
8+
branches:
9+
- main
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
publish:
17+
name: Publish Main
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
21+
packages: write
22+
steps:
23+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
24+
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
25+
with:
26+
go-version-file: v1-sync-helper/go.mod
27+
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
28+
with:
29+
version: v0.18.0
30+
- working-directory: ./v1-sync-helper
31+
run: |
32+
ko build github.com/linuxfoundation/lfx-v1-sync-helper \
33+
-B \
34+
--platform linux/amd64,linux/arm64 \
35+
-t ${{ github.sha }} \
36+
-t development \
37+
--sbom spdx
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright The Linux Foundation and each contributor to LFX.
2+
# SPDX-License-Identifier: MIT
3+
---
4+
name: Publish Tagged Release
5+
6+
"on":
7+
push:
8+
tags:
9+
- v*
10+
11+
env:
12+
COSIGN_VERSION: v2.6.1
13+
HELM_VERSION: v4.0.1
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
publish:
20+
name: Publish Tagged Release
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
packages: write
25+
outputs:
26+
app_version: ${{ steps.prepare.outputs.app_version }}
27+
chart_name: ${{ steps.prepare.outputs.chart_name }}
28+
chart_version: ${{ steps.prepare.outputs.chart_version }}
29+
steps:
30+
- name: Checkout repository
31+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
32+
- name: Prepare versions and chart name
33+
id: prepare
34+
run: |
35+
set -euo pipefail
36+
APP_VERSION=$(echo ${{ github.ref_name }} | sed 's/v//g')
37+
CHART_NAME="$(yq '.name' charts/*/Chart.yaml)"
38+
CHART_VERSION="$(yq '.version' charts/*/Chart.yaml)"
39+
{
40+
echo "app_version=$APP_VERSION"
41+
echo "chart_name=$CHART_NAME"
42+
echo "chart_version=$CHART_VERSION"
43+
} >> "$GITHUB_OUTPUT"
44+
45+
- name: Setup Go
46+
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
47+
with:
48+
go-version-file: v1-sync-helper/go.mod
49+
50+
- name: Setup Ko
51+
uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9
52+
with:
53+
version: v0.18.0
54+
55+
- name: Build and publish container image
56+
working-directory: ./v1-sync-helper
57+
run: |
58+
ko build github.com/linuxfoundation/lfx-v1-sync-helper \
59+
-B \
60+
--platform linux/amd64,linux/arm64 \
61+
-t ${{ github.ref_name }} \
62+
-t ${{ steps.prepare.outputs.app_version }} \
63+
-t latest \
64+
--sbom spdx
65+
66+
release-helm-chart:
67+
needs: publish
68+
runs-on: ubuntu-24.04
69+
permissions:
70+
contents: write
71+
packages: write
72+
id-token: write
73+
outputs:
74+
digest: ${{ steps.publish-ghcr.outputs.digest }}
75+
image_name: ${{ steps.publish-ghcr.outputs.image_name }}
76+
steps:
77+
- name: Checkout repository
78+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
79+
80+
- name: Publish Chart to GHCR
81+
id: publish-ghcr
82+
uses: >- # main
83+
linuxfoundation/lfx-public-workflows/.github/actions/helm-chart-oci-publisher@c465d6571fa0b8be9d551d902955164ea04a00af
84+
with:
85+
name: ${{ needs.publish.outputs.chart_name }}
86+
repository: ${{ github.repository }}/chart
87+
chart_version: ${{ needs.publish.outputs.chart_version }}
88+
app_version: ${{ needs.publish.outputs.app_version }}
89+
helm_version: "${{ env.HELM_VERSION }}"
90+
registry: ghcr.io
91+
registry_username: ${{ github.actor }}
92+
registry_password: ${{ secrets.GITHUB_TOKEN }}
93+
94+
- name: Install Cosign
95+
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
96+
with:
97+
cosign-release: "${{ env.COSIGN_VERSION }}"
98+
99+
- name: Login to GitHub
100+
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
101+
with:
102+
registry: ghcr.io
103+
username: ${{ github.actor }}
104+
password: ${{ secrets.GITHUB_TOKEN }}
105+
106+
- name: Sign the Helm chart in GHCR
107+
env:
108+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
109+
run: |
110+
set -euo pipefail
111+
cosign sign --yes '${{ steps.publish-ghcr.outputs.image_name }}@${{ steps.publish-ghcr.outputs.digest }}'
112+
113+
create-ghcr-helm-provenance:
114+
needs:
115+
- release-helm-chart
116+
permissions:
117+
actions: read
118+
id-token: write
119+
packages: write
120+
uses: >- # v2.1.0
121+
slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a
122+
with:
123+
image: ${{ needs.release-helm-chart.outputs.image_name }}
124+
digest: ${{ needs.release-helm-chart.outputs.digest }}
125+
registry-username: ${{ github.actor }}
126+
secrets:
127+
registry-password: ${{ secrets.GITHUB_TOKEN }}

charts/lfx-v1-sync-helper/Chart.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
---
44
apiVersion: v2
55
name: lfx-v1-sync-helper
6-
description: LFX Platform V1 Sync Helper chart
6+
description: LFX Platform v1 Sync Helper chart
77
type: application
88
version: 0.1.0
9-
appVersion: "latest"

v1-sync-helper/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The LFX v1 Sync Helper is a Go microservice that synchronizes v1 data from NATS
66

77
### Key implementation decisions
88

9-
1. **KV Bucket Watcher**: Instead of consuming NATS messages directly from streaming data sources, this service watches a NATS KV bucket (`v1-objects`) where V1 data is written by replication jobs (e.g. Meltano)
9+
1. **KV Bucket Watcher**: Instead of consuming NATS messages directly from streaming data sources, this service watches a NATS KV bucket (`v1-objects`) where v1 data is written by replication jobs (e.g. Meltano)
1010
2. **Direct API Calls**: With the exception of Meetings data, all data is routed into the LFX One platform via the appropriate API services, rather than the v1 Sync Helper writing directly to databases or platform-service queues.
1111
3. **JWT Authentication**: Reuses Heimdall's signing key to create JWT tokens for secure API authentication, supporting user impersonation while also bypassing LFX One permissions—as Heimdall tokens are not just proof of authentication, but are of *authorization*.
1212
4. **Mapping Storage**: Maintains v1-to-v2 ID mappings in a dedicated NATS KV bucket to track state and to avoid introducing "legacy ID" fields in LFX One data models.
@@ -107,7 +107,7 @@ indexer service).
107107
- **Subject**: `{client_id}` (without @clients suffix)
108108
- **Email**: Not included
109109

110-
**User Impersonation** (when V1 `lastmodifiedbyid` is User Service platform ID):
110+
**User Impersonation** (when v1 `lastmodifiedbyid` is User Service platform ID):
111111
- Looks up user via LFX v1 User Service API: `GET /v1/users/{platformID}`
112112
- **Principal**: `{username}` (from API response)
113113
- **Subject**: `{username}` (same as principal)
@@ -200,7 +200,7 @@ The service uses structured JSON logging with the following levels:
200200
- `operation`: KV operation type (PUT, DELETE)
201201
- `slug`/`sfid`: Object identifiers
202202
- `project_uid`/`committee_uid`: Generated V2 UUIDs
203-
- `username`: Extracted from V1 `lastmodifiedbyid`
203+
- `username`: Extracted from v1 `lastmodifiedbyid`
204204

205205
## License
206206

v1-sync-helper/go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
module v1-sync-helper
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
module github.com/linuxfoundation/lfx-v1-sync-helper
24

35
go 1.25.4
46

v1-sync-helper/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func shouldSkipSync(ctx context.Context, v1Data map[string]any) bool {
5555
return false
5656
}
5757

58-
// extractUserInfo extracts user information from V1 data for API calls and JWT impersonation.
58+
// extractUserInfo extracts user information from v1 data for API calls and JWT impersonation.
5959
func extractUserInfo(ctx context.Context, v1Data map[string]any, mappingsKV jetstream.KeyValue) UserInfo {
6060
// Extract platform ID from lastmodifiedbyid
6161
if lastModifiedBy, ok := v1Data["lastmodifiedbyid"].(string); ok && lastModifiedBy != "" {

v1-sync-helper/handlers_committees.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func handleCommitteeUpdate(ctx context.Context, key string, v1Data map[string]an
145145
// Update existing committee.
146146
logger.With("committee_uid", existingUID, "sfid", sfid).InfoContext(ctx, "updating existing committee")
147147

148-
// Map V1 data to update payload.
148+
// Map v1 data to update payload.
149149
var payload *committeeservice.UpdateCommitteeBasePayload
150150
payload, err = mapV1DataToCommitteeUpdateBasePayload(ctx, existingUID, v1Data, mappingsKV)
151151
if err != nil {
@@ -159,7 +159,7 @@ func handleCommitteeUpdate(ctx context.Context, key string, v1Data map[string]an
159159
// Create new committee.
160160
logger.With("sfid", sfid).InfoContext(ctx, "creating new committee")
161161

162-
// Map V1 data to create payload.
162+
// Map v1 data to create payload.
163163
var payload *committeeservice.CreateCommitteePayload
164164
payload, err = mapV1DataToCommitteeCreatePayload(ctx, v1Data, mappingsKV)
165165
if err != nil {
@@ -189,7 +189,7 @@ func handleCommitteeUpdate(ctx context.Context, key string, v1Data map[string]an
189189
logger.With("committee_uid", uid, "sfid", sfid).InfoContext(ctx, "successfully synced committee")
190190
}
191191

192-
// mapV1DataToCommitteeCreatePayload converts V1 committee data to a CreateCommitteePayload.
192+
// mapV1DataToCommitteeCreatePayload converts v1 committee data to a CreateCommitteePayload.
193193
func mapV1DataToCommitteeCreatePayload(ctx context.Context, v1Data map[string]any, mappingsKV jetstream.KeyValue) (*committeeservice.CreateCommitteePayload, error) {
194194
// Extract required fields.
195195
name := ""
@@ -263,7 +263,7 @@ func mapV1DataToCommitteeCreatePayload(ctx context.Context, v1Data map[string]an
263263
return payload, nil
264264
}
265265

266-
// mapV1DataToCommitteeUpdateBasePayload converts V1 committee data to an UpdateCommitteeBasePayload.
266+
// mapV1DataToCommitteeUpdateBasePayload converts v1 committee data to an UpdateCommitteeBasePayload.
267267
func mapV1DataToCommitteeUpdateBasePayload(ctx context.Context, committeeUID string, v1Data map[string]any, mappingsKV jetstream.KeyValue) (*committeeservice.UpdateCommitteeBasePayload, error) {
268268
// Extract required fields.
269269
name := ""
@@ -385,7 +385,7 @@ func handleCommitteeMemberUpdate(ctx context.Context, key string, v1Data map[str
385385
// Update existing committee member.
386386
logger.With("member_uid", existingMemberUID, "sfid", sfid, "committee_uid", committeeUID).InfoContext(ctx, "updating existing committee member")
387387

388-
// Map V1 data to update payload.
388+
// Map v1 data to update payload.
389389
var payload *committeeservice.UpdateCommitteeMemberPayload
390390
payload, err = mapV1DataToCommitteeMemberUpdatePayload(ctx, committeeUID, existingMemberUID, v1Data, mappingsKV)
391391
if err != nil {
@@ -399,7 +399,7 @@ func handleCommitteeMemberUpdate(ctx context.Context, key string, v1Data map[str
399399
// Create new committee member.
400400
logger.With("sfid", sfid, "committee_uid", committeeUID).InfoContext(ctx, "creating new committee member")
401401

402-
// Map V1 data to create payload.
402+
// Map v1 data to create payload.
403403
var payload *committeeservice.CreateCommitteeMemberPayload
404404
payload, err = mapV1DataToCommitteeMemberCreatePayload(ctx, committeeUID, v1Data, mappingsKV)
405405
if err != nil {
@@ -429,7 +429,7 @@ func handleCommitteeMemberUpdate(ctx context.Context, key string, v1Data map[str
429429
logger.With("member_uid", memberUID, "sfid", sfid, "committee_uid", committeeUID).InfoContext(ctx, "successfully synced committee member")
430430
}
431431

432-
// mapV1DataToCommitteeMemberCreatePayload converts V1 platform-community__c data to a CreateCommitteeMemberPayload.
432+
// mapV1DataToCommitteeMemberCreatePayload converts v1 platform-community__c data to a CreateCommitteeMemberPayload.
433433
func mapV1DataToCommitteeMemberCreatePayload(ctx context.Context, committeeUID string, v1Data map[string]any, mappingsKV jetstream.KeyValue) (*committeeservice.CreateCommitteeMemberPayload, error) {
434434
// Extract email field (already validated by caller).
435435
email := ""
@@ -593,7 +593,7 @@ func mapV1DataToCommitteeMemberCreatePayload(ctx context.Context, committeeUID s
593593
return payload, nil
594594
}
595595

596-
// mapV1DataToCommitteeMemberUpdatePayload converts V1 platform-community__c data to an UpdateCommitteeMemberPayload.
596+
// mapV1DataToCommitteeMemberUpdatePayload converts v1 platform-community__c data to an UpdateCommitteeMemberPayload.
597597
func mapV1DataToCommitteeMemberUpdatePayload(ctx context.Context, committeeUID, memberUID string, v1Data map[string]any, mappingsKV jetstream.KeyValue) (*committeeservice.UpdateCommitteeMemberPayload, error) {
598598
// Extract email field (already validated by caller).
599599
email := ""

v1-sync-helper/handlers_projects.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@ func handleProjectUpdate(ctx context.Context, key string, v1Data map[string]any,
100100
// Update existing project.
101101
logger.With("project_uid", existingUID, "sfid", sfid, "slug", slug).InfoContext(ctx, "updating existing project")
102102

103-
// Map V1 data to update payload.
103+
// Map v1 data to update payload.
104104
var payload *projectservice.UpdateProjectBasePayload
105105
payload, err = mapV1DataToProjectUpdateBasePayload(ctx, existingUID, v1Data, mappingsKV)
106106
if err != nil {
107107
logger.With(errKey, err, "sfid", sfid, "slug", slug).ErrorContext(ctx, "failed to map v1 data to update payload")
108108
return
109109
}
110110

111-
// Map V1 data to settings payload.
111+
// Map v1 data to settings payload.
112112
var settingsPayload *projectservice.UpdateProjectSettingsPayload
113113
settingsPayload, err = mapV1DataToProjectUpdateSettingsPayload(ctx, existingUID, v1Data)
114114
if err != nil {
@@ -122,7 +122,7 @@ func handleProjectUpdate(ctx context.Context, key string, v1Data map[string]any,
122122
// Create new project.
123123
logger.With("sfid", sfid, "slug", slug).InfoContext(ctx, "creating new project")
124124

125-
// Map V1 data to create payload.
125+
// Map v1 data to create payload.
126126
var payload *projectservice.CreateProjectPayload
127127
payload, err = mapV1DataToProjectCreatePayload(ctx, v1Data, mappingsKV)
128128
if err != nil {
@@ -152,7 +152,7 @@ func handleProjectUpdate(ctx context.Context, key string, v1Data map[string]any,
152152
logger.With("project_uid", uid, "sfid", sfid, "slug", slug).InfoContext(ctx, "successfully synced project")
153153
}
154154

155-
// mapV1DataToProjectCreatePayload converts V1 project data to a CreateProjectPayload.
155+
// mapV1DataToProjectCreatePayload converts v1 project data to a CreateProjectPayload.
156156
func mapV1DataToProjectCreatePayload(ctx context.Context, v1Data map[string]any, mappingsKV jetstream.KeyValue) (*projectservice.CreateProjectPayload, error) {
157157
// Extract required fields.
158158
name, nameOK := v1Data["name"].(string)
@@ -285,7 +285,7 @@ func mapV1DataToProjectCreatePayload(ctx context.Context, v1Data map[string]any,
285285
var checkPublicParentUID string
286286

287287
if parentProjectID != "" {
288-
// Project has a parent in V1, look up the parent's V2 UID from SFID mappings.
288+
// Project has a parent in v1, look up the parent's V2 UID from SFID mappings.
289289
parentMappingKey := fmt.Sprintf("project.sfid.%s", parentProjectID)
290290
if entry, err := mappingsKV.Get(ctx, parentMappingKey); err == nil {
291291
payload.ParentUID = string(entry.Value())
@@ -300,7 +300,7 @@ func mapV1DataToProjectCreatePayload(ctx context.Context, v1Data map[string]any,
300300
return nil, fmt.Errorf("could not find project parent UID in mappings for SFID %s", parentProjectID)
301301
}
302302
} else {
303-
// Project has no parent in V1, so it should be a child of ROOT in V2.
303+
// Project has no parent in v1, so it should be a child of ROOT in V2.
304304
rootUID, err := getRootProjectUID(ctx)
305305
if err != nil {
306306
return nil, fmt.Errorf("failed to get ROOT project UID: %w", err)
@@ -324,7 +324,7 @@ func mapV1DataToProjectCreatePayload(ctx context.Context, v1Data map[string]any,
324324
return payload, nil
325325
}
326326

327-
// mapV1DataToProjectUpdateBasePayload converts V1 project data to an UpdateProjectBasePayload.
327+
// mapV1DataToProjectUpdateBasePayload converts v1 project data to an UpdateProjectBasePayload.
328328
func mapV1DataToProjectUpdateBasePayload(ctx context.Context, projectUID string, v1Data map[string]any, mappingsKV jetstream.KeyValue) (*projectservice.UpdateProjectBasePayload, error) {
329329
// Extract required fields.
330330
name, nameOK := v1Data["name"].(string)
@@ -447,7 +447,7 @@ func mapV1DataToProjectUpdateBasePayload(ctx context.Context, projectUID string,
447447
var checkPublicParentUID string
448448

449449
if parentProjectID != "" {
450-
// Project has a parent in V1, look up the parent's V2 UID from SFID mappings.
450+
// Project has a parent in v1, look up the parent's V2 UID from SFID mappings.
451451
parentMappingKey := fmt.Sprintf("project.sfid.%s", parentProjectID)
452452
if entry, err := mappingsKV.Get(ctx, parentMappingKey); err == nil {
453453
payload.ParentUID = string(entry.Value())
@@ -462,7 +462,7 @@ func mapV1DataToProjectUpdateBasePayload(ctx context.Context, projectUID string,
462462
return nil, fmt.Errorf("could not find project parent UID in mappings for SFID %s", parentProjectID)
463463
}
464464
} else {
465-
// Project has no parent in V1, so it should be a child of ROOT in V2.
465+
// Project has no parent in v1, so it should be a child of ROOT in V2.
466466
rootUID, err := getRootProjectUID(ctx)
467467
if err != nil {
468468
return nil, fmt.Errorf("failed to get ROOT project UID: %w", err)
@@ -486,7 +486,7 @@ func mapV1DataToProjectUpdateBasePayload(ctx context.Context, projectUID string,
486486
return payload, nil
487487
}
488488

489-
// mapV1DataToProjectUpdateSettingsPayload converts V1 project data to an UpdateProjectSettingsPayload.
489+
// mapV1DataToProjectUpdateSettingsPayload converts v1 project data to an UpdateProjectSettingsPayload.
490490
func mapV1DataToProjectUpdateSettingsPayload(_ context.Context, projectUID string, v1Data map[string]any) (*projectservice.UpdateProjectSettingsPayload, error) {
491491
payload := &projectservice.UpdateProjectSettingsPayload{
492492
UID: &projectUID,

v1-sync-helper/lfx_v2_client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,8 @@ func generateCachedJWTToken(audience string, userInfo UserInfo) (string, error)
289289
// 2. Fallback Client: If no user info is provided, uses v1_sync_helper client credentials
290290
// with principal="v1_sync_helper@clients" and sub="v1_sync_helper"
291291
//
292-
// The impersonation approach allows V1 sync operations to be attributed to the actual
293-
// user who made the changes in V1, rather than a generic service account.
292+
// The impersonation approach allows v1 sync operations to be attributed to the actual
293+
// user who made the changes in v1, rather than a generic service account.
294294
func generateJWTToken(audience string, userInfo UserInfo) (string, error) {
295295
now := time.Now()
296296

0 commit comments

Comments
 (0)