From 5ff3d667b0133f4d7aaf04988055171f1dc6eef9 Mon Sep 17 00:00:00 2001 From: vSecurity Bot <112028320+vsecurity-github-bot@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:31:49 -0700 Subject: [PATCH 1/7] Install Security PR Check GitHub Action --- .github/workflows/security-pr-checks.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/security-pr-checks.yml diff --git a/.github/workflows/security-pr-checks.yml b/.github/workflows/security-pr-checks.yml new file mode 100644 index 0000000000..64be1aa0e3 --- /dev/null +++ b/.github/workflows/security-pr-checks.yml @@ -0,0 +1,13 @@ +name: security-pr-checks +on: + # Allow for manual run of security workflows + workflow_dispatch: + # Scan changed files in PRs (diff-aware scanning): + pull_request: {} +jobs: + running-pr-security-checks: + uses: verkada/securitybots/.github/workflows/pr-checks.yml@main + secrets: inherit + running-pr-semgrep-check: + uses: verkada/securitybots/.github/workflows/semgrep-pr-checks.yml@main + secrets: inherit From bea128af97f796c1401322e7beb6fc497102a18f Mon Sep 17 00:00:00 2001 From: Irena Liu Date: Wed, 5 Nov 2025 09:38:12 -0800 Subject: [PATCH 2/7] Add unit tests for resolved_with_pedigree and false_positive VEX statuses Signed-off-by: Irena Liu --- .../cyclonedx-vex-false-positive.json | 32 ----- .../cyclonedx-vex-resolved-with-pedigree.json | 32 ----- internal/testing/testdata/testdata.go | 112 +++++------------- 3 files changed, 30 insertions(+), 146 deletions(-) diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json b/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json index a530ad2ac3..2a35b67e5e 100644 --- a/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-false-positive.json @@ -45,38 +45,6 @@ ] } ] - }, - { - "id": "CVE-2024-0004", - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0004" - }, - "ratings": [ - { - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator" - }, - "score": 6.0, - "severity": "medium", - "method": "CVSSv31" - } - ], - "analysis": { - "state": "false_positive" - }, - "affects": [ - { - "ref": "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2-no-detail", - "versions": [ - { - "version": "1.0.0", - "status": "unaffected" - } - ] - } - ] } ] } diff --git a/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json b/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json index 24812ce25b..d5420774b7 100644 --- a/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json +++ b/internal/testing/testdata/exampledata/cyclonedx-vex-resolved-with-pedigree.json @@ -45,38 +45,6 @@ ] } ] - }, - { - "id": "CVE-2024-0003", - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-0003" - }, - "ratings": [ - { - "source": { - "name": "NVD", - "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator" - }, - "score": 7.5, - "severity": "high", - "method": "CVSSv31" - } - ], - "analysis": { - "state": "resolved_with_pedigree" - }, - "affects": [ - { - "ref": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component-no-detail", - "versions": [ - { - "version": "1.0.0", - "status": "affected" - } - ] - } - ] } ] } diff --git a/internal/testing/testdata/testdata.go b/internal/testing/testdata/testdata.go index 6172562909..c2acd7ed2a 100644 --- a/internal/testing/testdata/testdata.go +++ b/internal/testing/testdata/testdata.go @@ -326,22 +326,7 @@ var ( StatusNotes: "Vulnerability was falsely identified or associated with this component", KnownSince: time.Unix(0, 0).UTC(), } - // VexData for resolved_with_pedigree status without detail (maps to VexStatusFixed) - VexDataResolvedWithPedigreeNoDetail = &generated.VexStatementInputSpec{ - Status: generated.VexStatusFixed, - VexJustification: generated.VexJustificationNotProvided, - Statement: "", - StatusNotes: "CDX state: resolved_with_pedigree", - KnownSince: time.Unix(0, 0).UTC(), - } - // VexData for false_positive status without detail (maps to VexStatusNotAffected) - VexDataFalsePositiveNoDetail = &generated.VexStatementInputSpec{ - Status: generated.VexStatusNotAffected, - VexJustification: generated.VexJustificationNotProvided, - Statement: "", - StatusNotes: "CDX state: false_positive", - KnownSince: time.Unix(0, 0).UTC(), - } + // Vulnerability specs for new test cases VulnSpecResolvedWithPedigree = &generated.VulnerabilityInputSpec{ Type: "cve", VulnerabilityID: "cve-2024-0001", @@ -350,15 +335,6 @@ var ( Type: "cve", VulnerabilityID: "cve-2024-0002", } - // Vulnerability specs for no-detail test cases - VulnSpecResolvedWithPedigreeNoDetail = &generated.VulnerabilityInputSpec{ - Type: "cve", - VulnerabilityID: "cve-2024-0003", - } - VulnSpecFalsePositiveNoDetail = &generated.VulnerabilityInputSpec{ - Type: "cve", - VulnerabilityID: "cve-2024-0004", - } // VulnMetadata for resolved_with_pedigree test CycloneDXResolvedWithPedigreeVulnMetadata = []assembler.VulnMetadataIngest{ { @@ -369,14 +345,6 @@ var ( Timestamp: time.Unix(0, 0).UTC(), }, }, - { - Vulnerability: VulnSpecResolvedWithPedigreeNoDetail, - VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ - ScoreType: generated.VulnerabilityScoreTypeCvssv31, - ScoreValue: 7.5, - Timestamp: time.Unix(0, 0).UTC(), - }, - }, } // VulnMetadata for false_positive test CycloneDXFalsePositiveVulnMetadata = []assembler.VulnMetadataIngest{ @@ -388,14 +356,6 @@ var ( Timestamp: time.Unix(0, 0).UTC(), }, }, - { - Vulnerability: VulnSpecFalsePositiveNoDetail, - VulnMetadata: &generated.VulnerabilityMetadataInputSpec{ - ScoreType: generated.VulnerabilityScoreTypeCvssv31, - ScoreValue: 6.0, - Timestamp: time.Unix(0, 0).UTC(), - }, - }, } topLevelPkg, _ = asmhelpers.PurlToPkg("pkg:guac/cdx/ABC") @@ -427,7 +387,7 @@ var ( HasSBOM: &model.HasSBOMInputSpec{ Uri: "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", Algorithm: "sha256", - Digest: "32981b0c4f87df9243c0e9b8a9600f2e19aae0c0cb76122edfe4a54ef59b9d48", + Digest: "a9e5e5fcc0939b4e9ddf74a5863ff577bef9bbf8086d99a4dafb8154c451b56f", KnownSince: parseRfc3339("2024-01-15T10:30:00Z"), }, }, @@ -440,25 +400,22 @@ var ( HasSBOM: &model.HasSBOMInputSpec{ Uri: "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80", Algorithm: "sha256", - Digest: "0731373583749ae046d0992e9b417d4b2960f75d7a979c72fd0b7a258566d520", + Digest: "738690dd4acaf82b417072354ee631a20a50453278053b558770c6f65906f11d", KnownSince: parseRfc3339("2024-01-15T10:30:00Z"), }, }, } // Predicates for resolved_with_pedigree test + // The affects ref is "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79/1#test-component" + // The parser splits on "#" and uses "test-component" as pkdIdentifier + // Then creates PURL as pkg:guac/pkg/test-component@1.0.0 using guacCDXPkgPurl resolvedWithPedigreePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component@1.0.0") - resolvedWithPedigreeNoDetailPkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-no-detail@1.0.0") CycloneDXResolvedWithPedigreeVexIngest = []assembler.VexIngest{ { Pkg: resolvedWithPedigreePkg, Vulnerability: VulnSpecResolvedWithPedigree, VexData: VexDataResolvedWithPedigree, }, - { - Pkg: resolvedWithPedigreeNoDetailPkg, - Vulnerability: VulnSpecResolvedWithPedigreeNoDetail, - VexData: VexDataResolvedWithPedigreeNoDetail, - }, } CycloneDXResolvedWithPedigreePredicates = assembler.IngestPredicates{ HasSBOM: HasSBOMVexResolvedWithPedigree, @@ -467,24 +424,22 @@ var ( // Note: No CertifyVuln because status is Fixed (not Affected/UnderInvestigation) } // Predicates for false_positive test + // The affects ref is "urn:uuid:4e671687-395b-41f5-a30f-a58921a69b80/1#test-component-2" + // The parser splits on "#" and uses "test-component-2" as pkdIdentifier + // Then creates PURL as pkg:guac/pkg/test-component-2@1.0.0 using guacCDXPkgPurl falsePositivePkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-2@1.0.0") - falsePositiveNoDetailPkg, _ = asmhelpers.PurlToPkg("pkg:guac/pkg/test-component-2-no-detail@1.0.0") CycloneDXFalsePositiveVexIngest = []assembler.VexIngest{ { Pkg: falsePositivePkg, Vulnerability: VulnSpecFalsePositive, VexData: VexDataFalsePositive, }, - { - Pkg: falsePositiveNoDetailPkg, - Vulnerability: VulnSpecFalsePositiveNoDetail, - VexData: VexDataFalsePositiveNoDetail, - }, } CycloneDXFalsePositivePredicates = assembler.IngestPredicates{ HasSBOM: HasSBOMVexFalsePositive, VulnMetadata: CycloneDXFalsePositiveVulnMetadata, Vex: CycloneDXFalsePositiveVexIngest, + // Note: No CertifyVuln because status is NotAffected (not Affected/UnderInvestigation) } // DSSE/SLSA Testdata @@ -2400,36 +2355,29 @@ var ( }` VertxWebAttestation = `{ - "type": "https://in-toto.io/Statement/v1", - "subject": [ - { - "uri": "pkg:maven/io.vertx/vertx-web@4.3.7?type=jar" - } - ], - "predicate_type": "https://in-toto.io/attestation/vulns/v0.1", - "predicate": { - "scanner": { - "uri": "osv.dev", - "version": "0.0.14", - "db": {}, - "result": [ - { - "id": "GHSA-45p5-v273-3qqr" - }, + "type": "https://in-toto.io/Statement/v1", + "subject": [ { - "id": "GHSA-53jx-vvf9-4x38" + "uri": "pkg:maven/io.vertx/vertx-web@4.3.7?type=jar" + } + ], + "predicate_type": "https://in-toto.io/attestation/vulns/v0.1", + "predicate": { + "scanner": { + "uri": "osv.dev", + "version": "0.0.14", + "result": [ + { + "id": "GHSA-53jx-vvf9-4x38" + } + ] }, - { - "id": "GHSA-h5fg-jpgr-rv9c" + "metadata": { + "scanStartedOn":"2023-02-15T11:10:08.986506-08:00", + "scanFinishedOn":"2023-02-15T11:10:08.986506-08:00" } - ] - }, - "metadata": { - "scanStartedOn": "2025-12-01T15:19:40.545851224Z", - "scanFinishedOn": "2025-12-01T15:19:40.545851224Z" - } - } - }` + } + }` VertxWebCommonPackage = root_package.PackageNode{ Purl: "pkg:maven/io.vertx/vertx-web-common@4.3.7?type=jar", From 38fabf730c0a352e48d6e7b2b487b931901ab14b Mon Sep 17 00:00:00 2001 From: Irena Liu Date: Fri, 7 Nov 2025 15:31:46 -0800 Subject: [PATCH 3/7] handle empty analysis.state --- .../parser/cyclonedx/parser_cyclonedx.go | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go index 44b8d806a5..5fc5c66b9e 100644 --- a/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go +++ b/pkg/ingestor/parser/cyclonedx/parser_cyclonedx.go @@ -558,10 +558,17 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { var vd model.VexStatementInputSpec publishedTime := zeroTime if vulnerability.Analysis != nil { - if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { - vd.Status = vexStatus + // Only set status if state is not empty + if vulnerability.Analysis.State != "" { + if vexStatus, ok := vexStatusMap[vulnerability.Analysis.State]; ok { + vd.Status = vexStatus + } else { + return fmt.Errorf("unknown vulnerability status %s", vulnerability.Analysis.State) + } } else { - return fmt.Errorf("unknown vulnerability status %s", vulnerability.Analysis.State) + // If state is empty, use UNDER_INVESTIGATION to indicate analysis pending + // The affects array may indicate the component is affected, but VEX analysis hasn't been performed + vd.Status = model.VexStatusUnderInvestigation } if vexJustification, ok := justificationsMap[vulnerability.Analysis.Justification]; ok { @@ -571,9 +578,22 @@ func (c *cyclonedxParser) getVulnerabilities(ctx context.Context) error { } if vulnerability.Published != "" { + // Try RFC3339 first, then try various formats publishedTime, err = time.Parse(time.RFC3339, vulnerability.Published) if err != nil { - return fmt.Errorf("failed to pase time: %s, with error: %w", vulnerability.Published, err) + // Try RFC3339 without seconds (e.g., "2025-01-29T00:00Z") + publishedTime, err = time.Parse("2006-01-02T15:04Z", vulnerability.Published) + if err != nil { + // Try parsing without timezone (e.g., "2023-08-22T19:16:31.080") + publishedTime, err = time.Parse("2006-01-02T15:04:05.000", vulnerability.Published) + if err != nil { + // Try parsing without milliseconds + publishedTime, err = time.Parse("2006-01-02T15:04:05", vulnerability.Published) + if err != nil { + return fmt.Errorf("failed to parse time: %s, with error: %w", vulnerability.Published, err) + } + } + } } } vd.KnownSince = publishedTime From 599d6dbaaf5d613d23ed6211350e47a7a8d833f3 Mon Sep 17 00:00:00 2001 From: Irena Liu Date: Wed, 12 Nov 2025 13:15:08 -0800 Subject: [PATCH 4/7] Add simplified verkada release workflow - Create verkada-release.yaml workflow for verkada-specific tags - Add .goreleaser-verkada.yaml with minimal config: - Only builds binaries (no Docker images) - No signing, provenance, or Atlas - Faster build time (~3-5 minutes) - Generates guac_checksums.txt - Triggers on v*-verkada-* tags --- .github/workflows/verkada-release.yaml | 51 ++++++++++++ .goreleaser-verkada.yaml | 106 +++++++++++++++++++++++++ BUILD.bazel | 91 +++++++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100644 .github/workflows/verkada-release.yaml create mode 100644 .goreleaser-verkada.yaml create mode 100644 BUILD.bazel diff --git a/.github/workflows/verkada-release.yaml b/.github/workflows/verkada-release.yaml new file mode 100644 index 0000000000..545f21506f --- /dev/null +++ b/.github/workflows/verkada-release.yaml @@ -0,0 +1,51 @@ +# +# Copyright 2022 The GUAC Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: verkada-release + +on: + workflow_dispatch: # Manual trigger + push: + tags: + - "v*-verkada-*" + +permissions: + contents: write # To create releases and upload assets + +jobs: + build-binaries: + runs-on: ubuntu-latest + permissions: + contents: write # To upload release assets + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version: '1.24' + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + distribution: goreleaser + version: latest + args: release --clean -f .goreleaser-verkada.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser-verkada.yaml b/.goreleaser-verkada.yaml new file mode 100644 index 0000000000..fa897e4f18 --- /dev/null +++ b/.goreleaser-verkada.yaml @@ -0,0 +1,106 @@ +--- +project_name: guac +version: 2 + +env: + - CGO_ENABLED=0 + - PKG=github.com/guacsec/guac/pkg/version + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - main: ./cmd/guaccollect + id: guaccollect + binary: guaccollect-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guaccsub + id: guaccsub + binary: guaccsub-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guacgql + id: guacgql + binary: guacgql-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guacingest + id: guacingest + binary: guacingest-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['linux'] + goarch: + - amd64 + - arm64 + + - main: ./cmd/guacone + id: guacone + binary: guacone-{{ .Os }}-{{ .Arch }} + ldflags: + - -X {{.Env.PKG}}.Commit={{.FullCommit}} + - -X {{.Env.PKG}}.Date={{.Date}} + - -X {{.Env.PKG}}.Version={{.Summary}} + goos: ['darwin', 'linux'] + goarch: + - amd64 + - arm64 + +archives: + - formats: [binary] + name_template: "{{ .Binary }}" + allow_different_binary_count: true + +checksum: + name_template: "guac_checksums.txt" + +snapshot: + version_template: SNAPSHOT-{{ .ShortCommit }} + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +release: + prerelease: auto + draft: false + replace_existing_draft: true + name_template: "Verkada GUAC {{.Tag}}" + header: | + ## Verkada Custom GUAC Release + + This release contains custom modifications for Verkada's use case: + - VEX status mappings for Vigiles SBOM ingestion + - Enhanced empty analysis state handling + - Improved RFC3339 timestamp parsing + + Base version: GUAC v1.0.1 diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000000..d44e60d6f6 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,91 @@ +# BUILD.bazel for GUAC binaries +# This file builds GUAC binaries from the forked repo for use in vguac +# +# NOTE: This uses genrule to build binaries using `go build` directly. +# This requires go.mod and all dependencies to be available in the guac repo. +# For a more hermetic build, consider using gazelle to generate BUILD files. +# +# The binaries are built for linux/arm64 to match vguac's architecture requirements. + +package(default_visibility = ["//visibility:public"]) + +# GUAC GraphQL server binary +genrule( + name = "guacgql", + srcs = glob([ + "cmd/guacgql/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guacgql"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guacgql + """, + visibility = ["//visibility:public"], +) + +# GUAC Ingestor binary +genrule( + name = "guacingest", + srcs = glob([ + "cmd/guacingest/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guacingest"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guacingest + """, + visibility = ["//visibility:public"], +) + +# GUAC Collector binary +genrule( + name = "guaccollect", + srcs = glob([ + "cmd/guaccollect/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guaccollect"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guaccollect + """, + visibility = ["//visibility:public"], +) + +# GUAC CollectSub binary +genrule( + name = "guaccsub", + srcs = glob([ + "cmd/guaccsub/**/*.go", + "pkg/**/*.go", + "internal/**/*.go", + "go.mod", + "go.sum", + ]), + outs = ["guaccsub"], + cmd = """ + export GOOS=linux + export GOARCH=arm64 + cd $$(dirname $(location go.mod)) + go build -o $@ ./cmd/guaccsub + """, + visibility = ["//visibility:public"], +) + From 0470f2a0e7cd8f9b66898abf8e0e729256b6b118 Mon Sep 17 00:00:00 2001 From: Natsumi Pang Date: Fri, 14 Nov 2025 17:04:13 -0800 Subject: [PATCH 5/7] VSYS-13430 Update guac against remote automatically This updates verkada/guac to update against the remote fork automatically on a cron. --- .github/workflows/post-to-turnstile.yml | 182 ++++++++++++++++++ .github/workflows/verkada-update-release.yaml | 142 ++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 .github/workflows/post-to-turnstile.yml create mode 100644 .github/workflows/verkada-update-release.yaml diff --git a/.github/workflows/post-to-turnstile.yml b/.github/workflows/post-to-turnstile.yml new file mode 100644 index 0000000000..d0a10c1505 --- /dev/null +++ b/.github/workflows/post-to-turnstile.yml @@ -0,0 +1,182 @@ +name: Post to Turnstile +description: "Reusable workflow to post workflow results to S3 turnstile" + +on: + workflow_call: + inputs: + job_success: + description: "Whether the calling job succeeded" + required: true + type: boolean + job_result: + description: "The result status of the calling job" + required: true + type: string + workflow_name: + description: "Name of the calling workflow" + required: true + type: string + run_id: + description: "GitHub run ID" + required: true + type: string + run_number: + description: "GitHub run number" + required: true + type: string + start_time: + description: "When the job started (Unix timestamp)" + required: true + type: string + github_event_schedule: + description: "Cron schedule if triggered by schedule" + required: false + type: string + default: "" + github_repo_name: + description: "GitHub repository name" + required: false + type: string + default: "" + s3_bucket: + description: "S3 bucket for storing workflow results" + required: false + type: string + default: "verkada-device-platform-vlnx-ci" + aws_role_arn: + description: "AWS IAM role ARN to assume for S3 access" + required: true + type: string + +jobs: + PostToTurnstile: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v5.1.0 + with: + role-duration-seconds: 900 + role-to-assume: ${{ inputs.aws_role_arn }} + role-session-name: GithubActionTurnstile + aws-region: us-west-2 + + - name: Post to turnstile + env: + JOB_SUCCESS: ${{ inputs.job_success }} + JOB_RESULT: ${{ inputs.job_result }} + WORKFLOW_NAME: ${{ inputs.workflow_name }} + RUN_ID: ${{ inputs.run_id }} + RUN_NUMBER: ${{ inputs.run_number }} + START_TIME: ${{ inputs.start_time }} + GITHUB_EVENT_SCHEDULE: ${{ inputs.github_event_schedule }} + GITHUB_REPO_NAME: ${{ inputs.github_repo_name }} + GITHUB_SHA: ${{ github.sha }} + S3_BUCKET: ${{ inputs.s3_bucket }} + run: | + # Create temp directory + TEMP_DIR="$(mktemp -d)" + + # Capture end time for runtime calculation + END_TIME="$(date -u +%s)" + + # Calculate runtime in seconds if we have start time + if [[ "${START_TIME}" =~ ^[0-9]+$ ]]; then + # START_TIME is already a Unix timestamp + RUNTIME="$((${END_TIME} - ${START_TIME}))" + elif [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + # START_TIME is an ISO format string, convert to timestamp + START_EPOCH="$(date -u -d "${START_TIME}" +%s)" + RUNTIME="$((${END_TIME} - ${START_EPOCH}))" + else + RUNTIME=0 + fi + + # Determine success status from job result + SUCCESS=true + if [ "${JOB_SUCCESS}" != "true" ]; then + SUCCESS=false + fi + + # Use the provided cron schedule input directly + CRON_SCHEDULE="${GITHUB_EVENT_SCHEDULE}" + echo "Cron schedule: ${CRON_SCHEDULE}" + + # Validate required fields before creating JSON + echo "Validating required fields..." + + # Validate start_time is present and valid + if [ -z "${START_TIME}" ]; then + echo "❌ Error: start_time is required but missing" + exit 1 + fi + + # Validate start_time is a valid timestamp + if ! [[ "${START_TIME}" =~ ^[0-9]+$ ]] && ! [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + echo "❌ Error: start_time must be a Unix timestamp or ISO format" + exit 1 + fi + + # Validate cron_schedule is present and not empty + if [ -z "${CRON_SCHEDULE}" ]; then + echo "❌ Error: cron_schedule is required but missing" + exit 1 + fi + + echo "✅ All required fields validated" + + # Create workflow run URL + WORKFLOW_URL="https://github.com/${GITHUB_REPO_NAME}/actions/runs/${RUN_ID}" + + # Create JSON result file using jq to handle special characters safely + jq -n \ + --arg workflow "${WORKFLOW_NAME}" \ + --arg run_id "${RUN_ID}" \ + --arg run_number "${RUN_NUMBER}" \ + --arg repository "${GITHUB_REPO_NAME}" \ + --arg commit "${GITHUB_SHA}" \ + --arg started_at "${START_TIME}" \ + --arg completed_at "${END_TIME}" \ + --arg runtime_seconds "${RUNTIME}" \ + --arg success "${SUCCESS}" \ + --arg cron_schedule "${CRON_SCHEDULE}" \ + --arg workflow_url "${WORKFLOW_URL}" \ + '{ + workflow: $workflow, + run_id: $run_id, + run_number: $run_number, + repository: $repository, + commit: $commit, + started_at: $started_at, + completed_at: $completed_at, + runtime_seconds: $runtime_seconds, + success: $success, + cron_schedule: $cron_schedule, + workflow_url: $workflow_url + }' > "${TEMP_DIR}/result.json" + + # Upload to S3 + echo "Uploading result to S3..." + + # Strip "verkada/" prefix from repository name for cleaner S3 paths + if [[ "${GITHUB_REPO_NAME}" == "verkada/"* ]]; then + CLEAN_REPO_NAME="${GITHUB_REPO_NAME#verkada/}" + else + CLEAN_REPO_NAME="${GITHUB_REPO_NAME}" + fi + + # Sanitize workflow name to replace forward slashes with hyphens for clean S3 paths + CLEAN_WORKFLOW_NAME="${WORKFLOW_NAME//\//-}" + + S3_PREFIX="s3://${S3_BUCKET}/workflow_results/${CLEAN_REPO_NAME}/${CLEAN_WORKFLOW_NAME}" + S3_PATH="${S3_PREFIX}/result-${START_TIME}.json" + S3_PATH_LATEST="${S3_PREFIX}/result-latest.json" + + # Upload the JSON file + aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH}" + aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH_LATEST}" + + echo "Result uploaded to S3: ${S3_PATH}" + + # Clean up temp directory + rm -rf "${TEMP_DIR}" diff --git a/.github/workflows/verkada-update-release.yaml b/.github/workflows/verkada-update-release.yaml new file mode 100644 index 0000000000..00e8753ef4 --- /dev/null +++ b/.github/workflows/verkada-update-release.yaml @@ -0,0 +1,142 @@ +name: Update Real Remote to Fork + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + test_branch: + description: 'Optional: Branch to test with (leave empty to use main)' + required: false + type: string + schedule: + # run every night @ 01:00 am UTC + - cron: '1 0 * * *' + +jobs: + fetch-updates: + runs-on: ubuntu-latest + environment: upstream-link + outputs: + start_time: ${{ steps.set_start_time.outputs.time }} + + steps: + - name: Set branch name + id: set-branch + env: + TEST_BRANCH: ${{ github.event.inputs.test_branch }} + EVENT_NAME: ${{ github.event_name }} + GITHUB_REF_ENV: ${{ github.ref }} + run: | + if [ -n "${TEST_BRANCH}" ]; then + echo "branch_name=${TEST_BRANCH}" >> ${GITHUB_OUTPUT} + elif [ "${EVENT_NAME}" == "push" ]; then + echo "branch_name=${GITHUB_REF_ENV#refs/heads/}" >> ${GITHUB_OUTPUT} + else + echo "branch_name=main" >> ${GITHUB_OUTPUT} + fi + + - name: Checkout branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.set-branch.outputs.branch_name }} + submodules: recursive + fetch-depth: 0 + + - name: Set job start time + id: set_start_time + run: | + echo "time=$(date -u +%s)" >> $GITHUB_OUTPUT + echo "Set start time: $(date -u +%s)" + + - name: Update + env: + BRANCH_NAME: ${{ steps.set-branch.outputs.branch_name }} + UPSTREAM_URL: ${{ vars.UPSTREAM_URL }} + run: | + git fetch -p origin + git checkout "${BRANCH_NAME}" + + # Note: upstream needs to point to the origin repo + git remote add upstream "${UPSTREAM_URL}" + git fetch -p upstream + + if git diff --quiet origin/${BRANCH_NAME}..upstream/${BRANCH_NAME}; then + echo "No changes detected" + exit 0 + fi + + git rebase upstream/${BRANCH_NAME} + + - name: Push Updates + id: push-branch + env: + BRANCH_NAME: ${{ steps.set-branch.outputs.branch_name }} + run: | + git config --global user.email "device-platform-github-bot@noreply.verkada.com" + git config --global user.name "Device Platform GitHub Bot" + + git push + + - name: Create outcome file + run: | + jq -cn \ + --arg job_result "${{ job.status }}" \ + --arg workflow_name "${{ github.workflow }}" \ + --arg run_id "${{ github.run_id }}" \ + --arg run_number "${{ github.run_number }}" \ + --arg start_time "${{ steps.set_start_time.outputs.time }}" \ + --arg github_event_schedule "${{ github.event.schedule }}" \ + --arg github_repo_name "${{ github.repository }}" \ + '{ + job_success: ($job_result == "success"), + job_result: $job_result, + workflow_name: $workflow_name, + run_id: $run_id, + run_number: $run_number, + start_time: $start_time, + github_event_schedule: $github_event_schedule, + github_repo_name: $github_repo_name + }' > outcome + + - name: Post job failure status to slack + if: ${{ ! success() }} + uses: slackapi/slack-github-action@v1.24.0 + with: + # device-platform-alerts + channel-id: 'C083RN0EPEE' + # For posting a simple plain text message + slack-message: | + ${{ github.repository }}: ${{ github.workflow }}: *${{ job.status }}* + ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + <@U025XPBNJR5> <@U024C1144BE> <@U083E9XKYDN> + # nick, sam, natsumi + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + + - name: Upload outcome artifact + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: ${{ hashFiles('outcome') }} + path: outcome + + post-to-turnstile: + needs: [fetch-updates] + if: always() + environment: upstream-link + permissions: + id-token: write + contents: read + uses: ./.github/workflows/post-to-turnstile.yml + with: + job_success: ${{ needs.fetch-updates.result == 'success' }} + job_result: ${{ needs.fetch-updates.result }} + workflow_name: "${{ github.workflow }}" + run_id: ${{ github.run_id }} + run_number: ${{ github.run_number }} + start_time: ${{ needs.fetch-updates.outputs.start_time }} + github_event_schedule: ${{ github.event.schedule || '1 0 * * *' }} + github_repo_name: ${{ github.repository }} + aws_role_arn: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/vlnx-mirror-ci-github-action-role From 8bd622b6c6089699931813953ae6c9639460b822 Mon Sep 17 00:00:00 2001 From: Natsumi Pang Date: Tue, 2 Dec 2025 16:42:13 -0800 Subject: [PATCH 6/7] Fix Verkada Update Release Fix a few issues now that I figured out how to test changes not on main --- .github/workflows/ci-integration.yaml | 3 + .github/workflows/post-to-turnstile.yml | 182 ----------------- .github/workflows/release.yaml | 11 +- .github/workflows/verkada-update-release.yaml | 187 +++++++++++++----- 4 files changed, 148 insertions(+), 235 deletions(-) delete mode 100644 .github/workflows/post-to-turnstile.yml diff --git a/.github/workflows/ci-integration.yaml b/.github/workflows/ci-integration.yaml index b7fc9eda3d..37b5fe8efb 100644 --- a/.github/workflows/ci-integration.yaml +++ b/.github/workflows/ci-integration.yaml @@ -32,6 +32,7 @@ permissions: jobs: test-integration: + if: github.repository != 'verkada/guac' runs-on: ubuntu-latest name: CI for integration tests steps: @@ -65,6 +66,7 @@ jobs: run: make integration-test end-to-end: + if: github.repository != 'verkada/guac' name: E2E runs-on: ubuntu-latest services: @@ -119,6 +121,7 @@ jobs: GUAC_DIR: /home/runner/work/guac/guac tilt-ci: + if: github.repository != 'verkada/guac' name: Run 'tilt ci' runs-on: labels: ubuntu-latest diff --git a/.github/workflows/post-to-turnstile.yml b/.github/workflows/post-to-turnstile.yml deleted file mode 100644 index d0a10c1505..0000000000 --- a/.github/workflows/post-to-turnstile.yml +++ /dev/null @@ -1,182 +0,0 @@ -name: Post to Turnstile -description: "Reusable workflow to post workflow results to S3 turnstile" - -on: - workflow_call: - inputs: - job_success: - description: "Whether the calling job succeeded" - required: true - type: boolean - job_result: - description: "The result status of the calling job" - required: true - type: string - workflow_name: - description: "Name of the calling workflow" - required: true - type: string - run_id: - description: "GitHub run ID" - required: true - type: string - run_number: - description: "GitHub run number" - required: true - type: string - start_time: - description: "When the job started (Unix timestamp)" - required: true - type: string - github_event_schedule: - description: "Cron schedule if triggered by schedule" - required: false - type: string - default: "" - github_repo_name: - description: "GitHub repository name" - required: false - type: string - default: "" - s3_bucket: - description: "S3 bucket for storing workflow results" - required: false - type: string - default: "verkada-device-platform-vlnx-ci" - aws_role_arn: - description: "AWS IAM role ARN to assume for S3 access" - required: true - type: string - -jobs: - PostToTurnstile: - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: configure aws credentials - uses: aws-actions/configure-aws-credentials@v5.1.0 - with: - role-duration-seconds: 900 - role-to-assume: ${{ inputs.aws_role_arn }} - role-session-name: GithubActionTurnstile - aws-region: us-west-2 - - - name: Post to turnstile - env: - JOB_SUCCESS: ${{ inputs.job_success }} - JOB_RESULT: ${{ inputs.job_result }} - WORKFLOW_NAME: ${{ inputs.workflow_name }} - RUN_ID: ${{ inputs.run_id }} - RUN_NUMBER: ${{ inputs.run_number }} - START_TIME: ${{ inputs.start_time }} - GITHUB_EVENT_SCHEDULE: ${{ inputs.github_event_schedule }} - GITHUB_REPO_NAME: ${{ inputs.github_repo_name }} - GITHUB_SHA: ${{ github.sha }} - S3_BUCKET: ${{ inputs.s3_bucket }} - run: | - # Create temp directory - TEMP_DIR="$(mktemp -d)" - - # Capture end time for runtime calculation - END_TIME="$(date -u +%s)" - - # Calculate runtime in seconds if we have start time - if [[ "${START_TIME}" =~ ^[0-9]+$ ]]; then - # START_TIME is already a Unix timestamp - RUNTIME="$((${END_TIME} - ${START_TIME}))" - elif [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then - # START_TIME is an ISO format string, convert to timestamp - START_EPOCH="$(date -u -d "${START_TIME}" +%s)" - RUNTIME="$((${END_TIME} - ${START_EPOCH}))" - else - RUNTIME=0 - fi - - # Determine success status from job result - SUCCESS=true - if [ "${JOB_SUCCESS}" != "true" ]; then - SUCCESS=false - fi - - # Use the provided cron schedule input directly - CRON_SCHEDULE="${GITHUB_EVENT_SCHEDULE}" - echo "Cron schedule: ${CRON_SCHEDULE}" - - # Validate required fields before creating JSON - echo "Validating required fields..." - - # Validate start_time is present and valid - if [ -z "${START_TIME}" ]; then - echo "❌ Error: start_time is required but missing" - exit 1 - fi - - # Validate start_time is a valid timestamp - if ! [[ "${START_TIME}" =~ ^[0-9]+$ ]] && ! [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then - echo "❌ Error: start_time must be a Unix timestamp or ISO format" - exit 1 - fi - - # Validate cron_schedule is present and not empty - if [ -z "${CRON_SCHEDULE}" ]; then - echo "❌ Error: cron_schedule is required but missing" - exit 1 - fi - - echo "✅ All required fields validated" - - # Create workflow run URL - WORKFLOW_URL="https://github.com/${GITHUB_REPO_NAME}/actions/runs/${RUN_ID}" - - # Create JSON result file using jq to handle special characters safely - jq -n \ - --arg workflow "${WORKFLOW_NAME}" \ - --arg run_id "${RUN_ID}" \ - --arg run_number "${RUN_NUMBER}" \ - --arg repository "${GITHUB_REPO_NAME}" \ - --arg commit "${GITHUB_SHA}" \ - --arg started_at "${START_TIME}" \ - --arg completed_at "${END_TIME}" \ - --arg runtime_seconds "${RUNTIME}" \ - --arg success "${SUCCESS}" \ - --arg cron_schedule "${CRON_SCHEDULE}" \ - --arg workflow_url "${WORKFLOW_URL}" \ - '{ - workflow: $workflow, - run_id: $run_id, - run_number: $run_number, - repository: $repository, - commit: $commit, - started_at: $started_at, - completed_at: $completed_at, - runtime_seconds: $runtime_seconds, - success: $success, - cron_schedule: $cron_schedule, - workflow_url: $workflow_url - }' > "${TEMP_DIR}/result.json" - - # Upload to S3 - echo "Uploading result to S3..." - - # Strip "verkada/" prefix from repository name for cleaner S3 paths - if [[ "${GITHUB_REPO_NAME}" == "verkada/"* ]]; then - CLEAN_REPO_NAME="${GITHUB_REPO_NAME#verkada/}" - else - CLEAN_REPO_NAME="${GITHUB_REPO_NAME}" - fi - - # Sanitize workflow name to replace forward slashes with hyphens for clean S3 paths - CLEAN_WORKFLOW_NAME="${WORKFLOW_NAME//\//-}" - - S3_PREFIX="s3://${S3_BUCKET}/workflow_results/${CLEAN_REPO_NAME}/${CLEAN_WORKFLOW_NAME}" - S3_PATH="${S3_PREFIX}/result-${START_TIME}.json" - S3_PATH_LATEST="${S3_PREFIX}/result-latest.json" - - # Upload the JSON file - aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH}" - aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH_LATEST}" - - echo "Result uploaded to S3: ${S3_PATH}" - - # Clean up temp directory - rm -rf "${TEMP_DIR}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 33b6c14d4d..3919913729 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -28,6 +28,7 @@ permissions: jobs: goreleaser: + if: github.repository != 'verkada/guac' runs-on: ubuntu-latest outputs: hashes: ${{ steps.hash.outputs.hashes }} @@ -105,7 +106,7 @@ jobs: build-atlas: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: packages: write # To publish container images to GHCR id-token: write # To use our OIDC token @@ -140,7 +141,7 @@ jobs: name: generate sbom for container runs-on: ubuntu-latest needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: id-token: write # needed for signing the images with GitHub OIDC Token packages: write # needed to upload signatures @@ -177,7 +178,7 @@ jobs: provenance-bins: name: generate provenance for binaries needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: id-token: write # To sign the provenance contents: write # To upload assets to release @@ -190,7 +191,7 @@ jobs: provenance-container: name: generate provenance for container needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: id-token: write # To sign the provenance contents: write # To upload assets to release @@ -208,7 +209,7 @@ jobs: runs-on: ubuntu-latest name: generate compose tarball needs: [goreleaser] - if: startsWith(github.ref, 'refs/tags/') + if: github.repository != 'verkada/guac' && startsWith(github.ref, 'refs/tags/') permissions: contents: write # To upload assets to release. packages: write # To publish container images to GHCR diff --git a/.github/workflows/verkada-update-release.yaml b/.github/workflows/verkada-update-release.yaml index 00e8753ef4..d66b05dbe7 100644 --- a/.github/workflows/verkada-update-release.yaml +++ b/.github/workflows/verkada-update-release.yaml @@ -18,6 +18,10 @@ jobs: fetch-updates: runs-on: ubuntu-latest environment: upstream-link + timeout-minutes: 5 + permissions: + id-token: write + contents: write outputs: start_time: ${{ steps.set_start_time.outputs.time }} @@ -37,6 +41,14 @@ jobs: echo "branch_name=main" >> ${GITHUB_OUTPUT} fi + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v5.1.0 + with: + role-duration-seconds: 900 + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/vlnx-mirror-ci-github-action-role + role-session-name: GithubActionTurnstile + aws-region: us-west-2 + - name: Checkout branch uses: actions/checkout@v4 with: @@ -50,14 +62,21 @@ jobs: echo "time=$(date -u +%s)" >> $GITHUB_OUTPUT echo "Set start time: $(date -u +%s)" + - name: Configure git + run: | + git config --global user.email "device-platform-github-bot@noreply.verkada.com" + git config --global user.name "Device Platform GitHub Bot" + - name: Update env: BRANCH_NAME: ${{ steps.set-branch.outputs.branch_name }} UPSTREAM_URL: ${{ vars.UPSTREAM_URL }} + UPSTREAM_BRANCH: main run: | git fetch -p origin git checkout "${BRANCH_NAME}" - + + # Note: upstream needs to point to the origin repo git remote add upstream "${UPSTREAM_URL}" git fetch -p upstream @@ -74,32 +93,8 @@ jobs: env: BRANCH_NAME: ${{ steps.set-branch.outputs.branch_name }} run: | - git config --global user.email "device-platform-github-bot@noreply.verkada.com" - git config --global user.name "Device Platform GitHub Bot" - git push - - name: Create outcome file - run: | - jq -cn \ - --arg job_result "${{ job.status }}" \ - --arg workflow_name "${{ github.workflow }}" \ - --arg run_id "${{ github.run_id }}" \ - --arg run_number "${{ github.run_number }}" \ - --arg start_time "${{ steps.set_start_time.outputs.time }}" \ - --arg github_event_schedule "${{ github.event.schedule }}" \ - --arg github_repo_name "${{ github.repository }}" \ - '{ - job_success: ($job_result == "success"), - job_result: $job_result, - workflow_name: $workflow_name, - run_id: $run_id, - run_number: $run_number, - start_time: $start_time, - github_event_schedule: $github_event_schedule, - github_repo_name: $github_repo_name - }' > outcome - - name: Post job failure status to slack if: ${{ ! success() }} uses: slackapi/slack-github-action@v1.24.0 @@ -115,28 +110,124 @@ jobs: env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - - name: Upload outcome artifact + - name: Post to turnstile if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: ${{ hashFiles('outcome') }} - path: outcome + env: + JOB_SUCCESS: ${{ job.status == 'success' }} + JOB_RESULT: ${{ job.status }} + WORKFLOW_NAME: ${{ github.workflow }} + RUN_ID: ${{ github.run_id }} + RUN_NUMBER: ${{ github.run_number }} + START_TIME: ${{ steps.set_start_time.outputs.time }} + GITHUB_EVENT_SCHEDULE: ${{ github.event.schedule }} + GITHUB_REPO_NAME: ${{ github.repository }} + GITHUB_SHA: ${{ github.sha }} + S3_BUCKET: verkada-device-platform-vlnx-ci + run: | + # Create temp directory + TEMP_DIR="$(mktemp -d)" + + # Capture end time for runtime calculation + END_TIME="$(date -u +%s)" + + # Calculate runtime in seconds if we have start time + if [[ "${START_TIME}" =~ ^[0-9]+$ ]]; then + # START_TIME is already a Unix timestamp + RUNTIME="$((${END_TIME} - ${START_TIME}))" + elif [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + # START_TIME is an ISO format string, convert to timestamp + START_EPOCH="$(date -u -d "${START_TIME}" +%s)" + RUNTIME="$((${END_TIME} - ${START_EPOCH}))" + else + RUNTIME=0 + fi + + # Determine success status from job result + SUCCESS=true + if [ "${JOB_SUCCESS}" != "true" ]; then + SUCCESS=false + fi + + # Use the provided cron schedule input directly + CRON_SCHEDULE="${GITHUB_EVENT_SCHEDULE:-}" + echo "Cron schedule: ${CRON_SCHEDULE:-'not scheduled'}" + + # Validate required fields before creating JSON + echo "Validating required fields..." + + # Validate start_time is present and valid + if [ -z "${START_TIME}" ]; then + echo "❌ Error: start_time is required but missing" + exit 1 + fi + + # Validate start_time is a valid timestamp + if ! [[ "${START_TIME}" =~ ^[0-9]+$ ]] && ! [[ "${START_TIME}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then + echo "❌ Error: start_time must be a Unix timestamp or ISO format" + exit 1 + fi + + # Validate cron_schedule is present and not empty (only for scheduled runs) + if [ -z "${CRON_SCHEDULE}" ]; then + echo "⚠️ Warning: cron_schedule is empty (workflow not triggered by schedule)" + CRON_SCHEDULE="" + fi + + echo "✅ All required fields validated" + + # Create workflow run URL + WORKFLOW_URL="https://github.com/${GITHUB_REPO_NAME}/actions/runs/${RUN_ID}" + + # Create JSON result file using jq to handle special characters safely + jq -n \ + --arg workflow "${WORKFLOW_NAME}" \ + --arg run_id "${RUN_ID}" \ + --arg run_number "${RUN_NUMBER}" \ + --arg repository "${GITHUB_REPO_NAME}" \ + --arg commit "${GITHUB_SHA}" \ + --arg started_at "${START_TIME}" \ + --arg completed_at "${END_TIME}" \ + --arg runtime_seconds "${RUNTIME}" \ + --arg success "${SUCCESS}" \ + --arg cron_schedule "${CRON_SCHEDULE}" \ + --arg workflow_url "${WORKFLOW_URL}" \ + '{ + workflow: $workflow, + run_id: $run_id, + run_number: $run_number, + repository: $repository, + commit: $commit, + started_at: $started_at, + completed_at: $completed_at, + runtime_seconds: $runtime_seconds, + success: $success, + cron_schedule: $cron_schedule, + workflow_url: $workflow_url + }' > "${TEMP_DIR}/result.json" + + # Upload to S3 + echo "Uploading result to S3..." + + # Strip "verkada/" prefix from repository name for cleaner S3 paths + if [[ "${GITHUB_REPO_NAME}" == "verkada/"* ]]; then + CLEAN_REPO_NAME="${GITHUB_REPO_NAME#verkada/}" + else + CLEAN_REPO_NAME="${GITHUB_REPO_NAME}" + fi + + # Sanitize workflow name to replace forward slashes with hyphens for clean S3 paths + CLEAN_WORKFLOW_NAME="${WORKFLOW_NAME//\//-}" + + S3_PREFIX="s3://${S3_BUCKET}/workflow_results/${CLEAN_REPO_NAME}/${CLEAN_WORKFLOW_NAME}" + S3_PATH="${S3_PREFIX}/result-${START_TIME}.json" + S3_PATH_LATEST="${S3_PREFIX}/result-latest.json" + + # Upload the JSON file + aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH}" + aws s3 cp "${TEMP_DIR}/result.json" "${S3_PATH_LATEST}" + + echo "Result uploaded to S3: ${S3_PATH}" + + # Clean up temp directory + rm -rf "${TEMP_DIR}" - post-to-turnstile: - needs: [fetch-updates] - if: always() - environment: upstream-link - permissions: - id-token: write - contents: read - uses: ./.github/workflows/post-to-turnstile.yml - with: - job_success: ${{ needs.fetch-updates.result == 'success' }} - job_result: ${{ needs.fetch-updates.result }} - workflow_name: "${{ github.workflow }}" - run_id: ${{ github.run_id }} - run_number: ${{ github.run_number }} - start_time: ${{ needs.fetch-updates.outputs.start_time }} - github_event_schedule: ${{ github.event.schedule || '1 0 * * *' }} - github_repo_name: ${{ github.repository }} - aws_role_arn: arn:aws:iam::${{ secrets.AWS_ACCOUNT_NUMBER }}:role/vlnx-mirror-ci-github-action-role From 9f4642d6353865214257cd3b94e24542f0cf5d6a Mon Sep 17 00:00:00 2001 From: Natsumi Pang Date: Mon, 29 Dec 2025 14:11:54 -0800 Subject: [PATCH 7/7] Retry guac ingestor on nats failure Guac ingestor fails instantly when nats fails. This updates the subscribe call to retry. Fetches upstream testdata.go which seems to have been updated with test. --- cmd/guacingest/cmd/ingest.go | 59 ++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/cmd/guacingest/cmd/ingest.go b/cmd/guacingest/cmd/ingest.go index 40f9fbcb57..b20376bdf7 100644 --- a/cmd/guacingest/cmd/ingest.go +++ b/cmd/guacingest/cmd/ingest.go @@ -26,6 +26,7 @@ import ( "strings" "sync" "syscall" + "time" "github.com/guacsec/guac/pkg/blob" "github.com/guacsec/guac/pkg/cli" @@ -91,11 +92,27 @@ func ingest(cmd *cobra.Command, args []string) { } if strings.HasPrefix(opts.pubsubAddr, "nats://") { - // initialize jetstream + // initialize jetstream with retry logic // TODO: pass in credentials file for NATS secure login jetStream := emitter.NewJetStream(opts.pubsubAddr, "", "") - if err := jetStream.JetStreamInit(ctx); err != nil { - logger.Fatalf("jetStream initialization failed with error: %v", err) + + const ( + initialBackoff = 1 * time.Second + maxBackoff = 30 * time.Second + ) + backoff := initialBackoff + + for { + if err := jetStream.JetStreamInit(ctx); err != nil { + logger.Warnf("jetStream initialization failed: %v, retrying in %v", err, backoff) + time.Sleep(backoff) + backoff = backoff * 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + continue + } + break } defer jetStream.Close() } @@ -145,9 +162,39 @@ func ingest(cmd *cobra.Command, args []string) { wg.Add(1) go func() { defer wg.Done() - if err := process.Subscribe(ctx, emit, blobStore, pubsub); err != nil { - logger.Errorf("processor ended with error: %v", err) - sigs <- syscall.SIGTERM + const ( + initialBackoff = 1 * time.Second + maxBackoff = 60 * time.Second + backoffFactor = 2.0 + ) + backoff := initialBackoff + + for { + err := process.Subscribe(ctx, emit, blobStore, pubsub) + if err != nil { + // Check if context was cancelled (graceful shutdown) + if errors.Is(err, context.Canceled) { + logger.Infof("processor shutting down gracefully") + return + } + + logger.Errorf("processor ended with error: %v, retrying in %v", err, backoff) + + select { + case <-ctx.Done(): + logger.Infof("processor context cancelled during backoff") + return + case <-time.After(backoff): + // Increase backoff for next retry, capped at maxBackoff + backoff = time.Duration(float64(backoff) * backoffFactor) + if backoff > maxBackoff { + backoff = maxBackoff + } + } + } else { + // Subscribe returned without error (clean shutdown) + return + } } }()