diff --git a/.github/workflows/auto_release.yml b/.github/workflows/auto_release.yml deleted file mode 100644 index 59fcc79f3..000000000 --- a/.github/workflows/auto_release.yml +++ /dev/null @@ -1,168 +0,0 @@ -name: Auto Release - -on: - push: - tags: - - "v*" - -concurrency: - group: deploy-prod - cancel-in-progress: false - -# Top-level token is read-only; each job declares the writes it needs. -# A job-level permissions block replaces the whole set — every block -# must re-state contents: read for checkout. -permissions: - contents: read - -jobs: - validate: - if: github.actor != 'vault-cortex-release[bot]' - runs-on: ubuntu-latest - outputs: - version: ${{ steps.tag.outputs.version }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Extract version from tag - id: tag - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - - - name: Validate package.json version matches tag - env: - TAG_VERSION: ${{ steps.tag.outputs.version }} - run: | - PKG_VERSION=$(jq -r '.version' package.json) - if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then - echo "::error::package.json version ($PKG_VERSION) does not match tag (v$TAG_VERSION)" - exit 1 - fi - echo "package.json version matches tag v$TAG_VERSION" - - deploy: - needs: validate - # Ceiling for the reusable workflow: AWS OIDC + GHCR push. - permissions: - id-token: write - contents: read - packages: write - uses: ./.github/workflows/deploy.yml - with: - version: ${{ needs.validate.outputs.version }} - secrets: inherit - - publish-registry: - needs: [validate, deploy] - # Ceiling for the reusable workflow: MCP Registry OIDC. - permissions: - id-token: write - contents: read - uses: ./.github/workflows/publish-registry.yml - with: - version: ${{ needs.validate.outputs.version }} - - sync-dockerhub-description: - needs: [validate, deploy] - permissions: - contents: read - uses: ./.github/workflows/dockerhub-description.yml - secrets: - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - - release: - needs: [validate, deploy] - runs-on: ubuntu-latest - # gh release create uses GITHUB_TOKEN; the CHANGELOG push to main - # authenticates as the release App, not this token. generate-notes.sh - # reads merged PRs (labels/body) for breaking-change detection, so the - # token also needs pull-requests: read. - permissions: - contents: write - pull-requests: read - id-token: write # cosign keyless signing via Fulcio OIDC - steps: - - name: Generate App token - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - token: ${{ steps.app-token.outputs.token }} - - - name: Generate release notes - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bash .github/scripts/generate-notes.sh "$GITHUB_REF_NAME" > /tmp/release-notes.md - - - name: Update changelog - env: - VERSION: ${{ needs.validate.outputs.version }} - run: bash .github/scripts/update-changelog.sh "$VERSION" /tmp/release-notes.md - - - name: Commit changelog update - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git stash - git checkout main - git stash pop - git add CHANGELOG.md - if git diff --cached --quiet; then - echo "No CHANGELOG.md changes to commit" - exit 0 - fi - git commit -m "docs: update CHANGELOG.md for $GITHUB_REF_NAME" - git push origin main - - # Idempotent: skip creation if the release already exists so a - # re-run after a signing failure proceeds to the cosign steps. - - name: Create GitHub release - timeout-minutes: 2 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if gh release view "$GITHUB_REF_NAME" --json tagName > /dev/null 2>&1; then - echo "Release $GITHUB_REF_NAME already exists, skipping creation" - exit 0 - fi - for i in 1 2 3; do - if gh release create "$GITHUB_REF_NAME" \ - --title "$GITHUB_REF_NAME" \ - --notes-file /tmp/release-notes.md; then - exit 0 - fi - echo "Attempt $i failed, retrying in 10s..." - sleep 10 - done - echo "Failed to create release after 3 attempts" - exit 1 - - - name: Install cosign - if: ${{ needs.deploy.outputs.digest-local != '' }} - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - - - name: Create digest file - if: ${{ needs.deploy.outputs.digest-local != '' }} - env: - IMAGE: ghcr.io/${{ vars.GHCR_USER }}/vault-cortex - VERSION: ${{ needs.validate.outputs.version }} - DIGEST_LOCAL: ${{ needs.deploy.outputs.digest-local }} - DIGEST_REMOTE: ${{ needs.deploy.outputs.digest-remote }} - run: | - printf '%s %s\n' "$DIGEST_LOCAL" "$IMAGE:v$VERSION" > digests.txt - printf '%s %s\n' "$DIGEST_REMOTE" "$IMAGE:v$VERSION-remote" >> digests.txt - cat digests.txt - - - name: Sign digest file - if: ${{ needs.deploy.outputs.digest-local != '' }} - run: cosign sign-blob --yes --bundle digests.txt.sigstore.json digests.txt - - - name: Upload signed digests to release - if: ${{ needs.deploy.outputs.digest-local != '' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release upload --clobber "$GITHUB_REF_NAME" digests.txt digests.txt.sigstore.json diff --git a/.github/workflows/cli_release.yml b/.github/workflows/cli_release.yml index e1a56e4a6..9dd2702c6 100644 --- a/.github/workflows/cli_release.yml +++ b/.github/workflows/cli_release.yml @@ -30,8 +30,8 @@ on: # Shares the server release/deploy group: both release flows push to # main, so running them at once can reject one push. A run dispatched # while a server release is in flight queues until it's done. -# GitHub keeps one queued run per group, so any later run in the group (a -# release or deploy dispatch, or a v* tag push) cancels the queued one. +# GitHub keeps one queued run per group, so a later release or deploy +# dispatch cancels the queued one; dispatch the cancelled run again. concurrency: group: deploy-prod cancel-in-progress: false diff --git a/.github/workflows/manual_release.yml b/.github/workflows/manual_release.yml index 4166a36b2..fd4e3d578 100644 --- a/.github/workflows/manual_release.yml +++ b/.github/workflows/manual_release.yml @@ -21,10 +21,10 @@ on: type: boolean default: false -# Shared with the other release workflows, which also push to main, and -# the deploy workflows. GitHub keeps one queued run per group, so any -# later run in the group (a release or deploy dispatch, or a v* tag push) -# cancels the queued one. +# Shared with cli_release.yml, which also pushes to main, and with +# force_deploy.yml and test_deploy.yml, which deploy to the same instance. +# GitHub keeps one queued run per group, so a later release or deploy +# dispatch cancels the queued one; dispatch the cancelled run again. concurrency: group: deploy-prod cancel-in-progress: false diff --git a/.github/workflows/publish-registry.yml b/.github/workflows/publish-registry.yml index e84ed8057..dc096ba15 100644 --- a/.github/workflows/publish-registry.yml +++ b/.github/workflows/publish-registry.yml @@ -29,10 +29,9 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # publish reads server.json only; don't persist the token to .git/config - # The bumped commit is tagged before this job runs in both release - # paths (auto_release is triggered by the tag; manual_release pushes - # the tag in bump-and-tag, which deploy + this job depend on), so the - # tag always resolves to the version-bumped server.json. + # manual_release.yml pushes this tag in its bump-and-tag job before + # it calls this workflow, so the tag should resolve to the + # version-bumped server.json; the next step stops the run if not. ref: v${{ inputs.version }} - name: Verify server.json version matches release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a16ff90ce..71b50e88f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -224,18 +224,13 @@ the allowance states why. ## Release Process -Releases are cut by the maintainer, in one of two ways: +Releases are cut by the maintainer: Actions tab → "Manual Release" → choose +`patch`/`minor`/`major`. The workflow bumps the version, deploys, and creates +the GitHub Release. -- **Manual Release:** Actions tab → "Manual Release" → choose - `patch`/`minor`/`major`. The workflow bumps the version, deploys, and - creates the GitHub Release. -- **Tag push:** merge a version-bump PR into `main`, then run - `git tag v && git push --tags`. The tag push deploys and creates - the GitHub Release. - -Direct commits to `main` are blocked by a branch ruleset — all changes, a tag -push's version bump included, land via PR. Only the release workflows push to -`main` directly, for their version-bump and CHANGELOG commits. +Direct commits to `main` are blocked by a branch ruleset — all changes land +via PR. Only the release workflows push to `main` directly, for their +version-bump and CHANGELOG commits. The CLI releases separately: Actions tab → "Release CLI" (see [The `cli/` Package](#the-cli-package)). @@ -249,11 +244,12 @@ What each release's notes list: change only `devDependencies` or `overrides` in the root `package.json`, and in `package-lock.json` only packages marked `"dev": true` - Claude Code hook changes (commit scope `hooks`) - - commits that change only `AGENTS.md`, `CHANGELOG.md`, dotfiles at the - repository root (`.gitignore`, `.trivyignore`; not `.dockerignore`, which - decides what the image build copies, or `.env.example`, which DEPLOY.md - has deployers copy), or files under `.claude/`, - `.devin/` or `.husky/` + - commits that change only these files: + - `AGENTS.md` and `CHANGELOG.md` + - dotfiles at the repository root, such as `.gitignore` and + `.trivyignore`, except `.dockerignore` (it decides what the image + build copies) and `.env.example` (DEPLOY.md has deployers copy it) + - files under `.claude/`, `.devin/` or `.husky/` - **CLI releases** (`.github/scripts/generate-cli-notes.sh`): every commit that changes `cli/`, including dependency bumps that edit `cli/package.json`, except commits whose added and removed lines under diff --git a/DEPLOY.md b/DEPLOY.md index d208cb41f..d7fab0dbf 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -241,30 +241,29 @@ Afterwards restore the three lines so the edit is never committed. ## CI/CD -GitHub Actions runs lint/test/build plus security scans (secret detection, image vulnerabilities) on every PR and push to main, and handles releases via tag push or manual dispatch. CI deploys land on the same Lightsail instance as your laptop deploys (the `SST_STAGE` repo secret pins the SST stage). +GitHub Actions runs lint/test/build plus security scans (secret detection, image vulnerabilities) on every PR and push to main, and handles releases via manual dispatch. CI deploys land on the same Lightsail instance as your laptop deploys (the `SST_STAGE` repo secret pins the SST stage). ### Workflows -| Workflow | Trigger | What it does | -| --------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ci.yml` | PR + push to main | `prettier:check`, `lint`, `markdownlint`, `lint:shell`, `knip`, `test`, `build` | -| `gitleaks.yml` | PR + push to main | Secret detection across the repo and its git history | -| `trivy.yml` | PR + push to main + weekly cron | Vulnerability scan of the Docker image — PRs scan an image built from the branch (`trivy-pr` gates merges on fixable CRITICAL/HIGH findings); pushes and the cron scan the published GHCR `:latest` and `:remote` images report-only, at every severity. Findings upload as SARIF to the Security tab. | -| `scorecard.yml` | Push to main + weekly cron | [OpenSSF Scorecard](https://github.com/ossf/scorecard) supply-chain posture analysis; results upload to the Security tab. Also re-runs when branch protection settings change. | -| `auto_release.yml` | `v*` tag push (from your laptop) | Validates `package.json` version matches the tag → calls `deploy.yml` + `publish-registry.yml` → creates a GitHub Release with auto-generated notes and updates `CHANGELOG.md` | -| `manual_release.yml` | Actions UI (`workflow_dispatch`) | Bumps version, commits, tags, pushes, calls `deploy.yml` + `publish-registry.yml`, creates the GitHub Release — all inline. Does NOT chain through `auto_release.yml` (a workflow-pushed tag can't trigger another workflow). | -| `deploy.yml` | Reusable (`workflow_call`) | OIDC AWS auth → `sst deploy` → Docker build/push to GHCR + Docker Hub → SSH to Lightsail → `docker compose pull && up -d` → `/healthz` gate → host health report (non-blocking warning annotations when the instance's apt is broken, a security-update backlog has built up, or a kernel update awaits a reboot; `test_deploy.yml` runs the same step) | -| `force_deploy.yml` | Actions UI (`workflow_dispatch`) | Redeploys the current release with fresh configuration — no new image build. Re-runs SST, rewrites `.env` from current secrets/vars, restarts compose. Use after rotating a secret or repo variable. | -| `test_deploy.yml` | Actions UI (`workflow_dispatch`) | Deploys the selected branch to the instance for testing without cutting a release — builds an amd64-only `remote`-target image pushed as `:test` (never `:remote`, `:latest`, or version tags, so released users are unaffected) and points the instance at it via `VAULT_CORTEX_TAG=test`. Run Force Deploy afterwards to return to the released image. | -| `publish-registry.yml` | Reusable (`workflow_call`) | Publishes `server.json` to the [official MCP Registry](https://registry.modelcontextprotocol.io/) via `mcp-publisher`, authenticating with GitHub OIDC. Runs after `deploy` (so the GHCR image referenced in `server.json` already exists). | -| `cli_release.yml` | Actions UI (`workflow_dispatch`) | Publishes the `cli/` package to npm via Trusted Publishing — independent of server releases. See [CONTRIBUTING.md](./CONTRIBUTING.md#the-cli-package). | -| `cli_pty_tests.yml` | Actions UI (`workflow_dispatch`) | Runs the CLI interactive PTY integration tests via node-pty. Also runs as a pre-publish gate in `cli_release.yml`. | -| `ghcr-cleanup.yml` | Weekly cron + Actions UI | Deletes untagged GHCR image digests to reclaim storage. Manual runs default to dry-run mode. | -| `dockerhub-description.yml` | Reusable (`workflow_call`) | Syncs `DOCKERHUB.md` to the Docker Hub repository description via `peter-evans/dockerhub-description`. Called by both release workflows after `deploy` — keeps the Hub listing in sync on every release. | - -> **Why two release paths?** GitHub prevents a tag pushed by `GITHUB_TOKEN` from inside a workflow from triggering other workflows. So `manual_release.yml` has to do its own deploy + release inline instead of relying on `auto_release.yml` firing. `auto_release.yml` still exists for the laptop path — when you push a tag from your terminal, your user account is the actor and the trigger fires normally. +| Workflow | Trigger | What it does | +| --------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ci.yml` | PR + push to main | `prettier:check`, `lint`, `markdownlint`, `lint:shell`, `knip`, `test`, `build` | +| `gitleaks.yml` | PR + push to main | Secret detection across the repo and its git history | +| `trivy.yml` | PR + push to main + weekly cron | Vulnerability scan of the Docker image — PRs scan an image built from the branch (`trivy-pr` gates merges on fixable CRITICAL/HIGH findings); pushes and the cron scan the published GHCR `:latest` and `:remote` images report-only, at every severity. Findings upload as SARIF to the Security tab. | +| `scorecard.yml` | Push to main + weekly cron | [OpenSSF Scorecard](https://github.com/ossf/scorecard) supply-chain posture analysis; results upload to the Security tab. Also re-runs when branch protection settings change. | +| `manual_release.yml` | Actions UI (`workflow_dispatch`) | Bumps version, commits, tags, pushes → calls `deploy.yml` → runs three jobs side by side: `publish-registry.yml`, `dockerhub-description.yml`, and a release job that updates `CHANGELOG.md`, creates the GitHub Release with notes from `.github/scripts/generate-notes.sh`, and attaches the signed image digests (see [Release Signing](./SECURITY.md#release-signing)) | +| `deploy.yml` | Reusable (`workflow_call`) | OIDC AWS auth → `sst deploy` → Docker build/push to GHCR + Docker Hub → SSH to Lightsail → `docker compose pull && up -d` → `/healthz` gate → host health report (non-blocking warning annotations when the instance's apt is broken, a security-update backlog has built up, or a kernel update awaits a reboot; `test_deploy.yml` runs the same step) | +| `force_deploy.yml` | Actions UI (`workflow_dispatch`) | Redeploys the current release with fresh configuration — no new image build. Re-runs SST, rewrites `.env` from current secrets/vars, restarts compose. Use after rotating a secret or repo variable. | +| `test_deploy.yml` | Actions UI (`workflow_dispatch`) | Deploys the selected branch to the instance for testing without cutting a release — builds an amd64-only `remote`-target image pushed as `:test` (never `:remote`, `:latest`, or version tags, so released users are unaffected) and points the instance at it via `VAULT_CORTEX_TAG=test`. Run Force Deploy afterwards to return to the released image. | +| `publish-registry.yml` | Reusable (`workflow_call`) | Publishes `server.json` to the [official MCP Registry](https://registry.modelcontextprotocol.io/) via `mcp-publisher`, authenticating with GitHub OIDC. Runs after `deploy` (so the GHCR image referenced in `server.json` already exists). | +| `cli_release.yml` | Actions UI (`workflow_dispatch`) | Publishes the `cli/` package to npm via Trusted Publishing — independent of server releases. See [CONTRIBUTING.md](./CONTRIBUTING.md#the-cli-package). | +| `cli_pty_tests.yml` | Actions UI (`workflow_dispatch`) | Runs the CLI interactive PTY integration tests via node-pty. Also runs as a pre-publish gate in `cli_release.yml`. | +| `ghcr-cleanup.yml` | Weekly cron + Actions UI | Deletes untagged GHCR image digests to reclaim storage. Manual runs default to dry-run mode. | +| `dockerhub-description.yml` | Reusable (`workflow_call`) | Syncs `DOCKERHUB.md` to the Docker Hub repository description via `peter-evans/dockerhub-description`. Called by `manual_release.yml` after `deploy` — keeps the Hub listing in sync on every release. | + +> **MCP Registry publishing is automatic.** `manual_release.yml` calls `publish-registry.yml`, so the [official MCP Registry](https://registry.modelcontextprotocol.io/) entry tracks every release — no manual `mcp-publisher publish`. It authenticates with [GitHub OIDC](https://modelcontextprotocol.io/registry/github-actions) and needs **no secret** (unlike the AWS OIDC role below, which you must provision): the registry authorizes the `io.github./*` namespace from the OIDC token's repo owner. > -> **MCP Registry publishing is automatic.** Both release paths call `publish-registry.yml`, so the [official MCP Registry](https://registry.modelcontextprotocol.io/) entry tracks every release — no manual `mcp-publisher publish`. It authenticates with [GitHub OIDC](https://modelcontextprotocol.io/registry/github-actions) and needs **no secret**: the registry authorizes the `io.github./*` namespace from the OIDC token's repo owner, so a fork publishing `io.github./...` works out of the box (unlike the AWS OIDC role below, which you must provision). +> A fork's publish job fails until the fork renames the server under its own `io.github./` namespace: `io.github.aliasunder/vault-cortex` is hardcoded in `server.json`'s `name` and the `Dockerfile`'s `io.modelcontextprotocol.server.name` label, and the bump step in `manual_release.yml` writes the `ghcr.io/aliasunder/vault-cortex` image path into `server.json`. The job has no off switch, but a failed publish does not stop the GitHub Release. ### GitHub OIDC setup (for forkers) @@ -407,11 +406,9 @@ Both halves come from the dedicated deploy keypair set up in [Prerequisites](#pr ### Cutting a release -**Manual** — Actions tab → "Manual Release" → Run workflow → choose `patch`/`minor`/`major`. The job bumps `package.json`, commits, tags, deploys, and creates the GitHub Release — all inline (a workflow-pushed tag can't trigger `auto_release.yml`; see [Workflows](#workflows)). +Actions tab → "Manual Release" → Run workflow → choose `patch`/`minor`/`major`. The workflow bumps the version in `package.json`, `package-lock.json`, and `server.json`, commits, tags, deploys, and creates the GitHub Release. Tick `skip_deploy` when restarting compose would overwrite a manually deployed stack: the Docker image is still built and pushed, but the SST deploy and the instance update are skipped. -**Tag push** — merge a version-bump PR into `main`, then tag the merge commit: `git tag v && git push --tags`. The tag push triggers `auto_release.yml`, which deploys and creates the release. - -Direct commits to `main` are blocked by a branch ruleset — every change, a tag push's version bump included, lands via PR. Release automation is the only actor that pushes to `main` directly (the changelog and version-bump commits in the workflows above). +Direct commits to `main` are blocked by a branch ruleset — every change lands via PR. Release automation is the only actor that pushes to `main` directly: `manual_release.yml` pushes the version-bump and CHANGELOG commits, and `cli_release.yml` pushes the CLI's version-bump commit. ### Rotating SSH keys @@ -440,14 +437,14 @@ ssh -i ~/.ssh/vault-cortex ubuntu@ \ Rotation ends every OAuth session — each connected client goes back through the consent page on its next request, and clients that use the token directly need the new value. -The token must stay in sync across four places: the SST secret (`npm run sst -- secret set McpAuthToken`), the GitHub repo secret, `~/.config/vault-cortex/.env` (which `npm run lightsail:up` copies to the instance), and the instance `.env`. CI writes the instance `.env` from the GitHub secret on every deploy, so the laptop rotation procedure becomes: +The token must stay in sync across four places: the SST secret (`npm run sst -- secret set McpAuthToken`), the GitHub repo secret, `~/.config/vault-cortex/.env` (which `npm run lightsail:up` copies to the instance), and the instance `.env`. CI writes the instance `.env` from the GitHub secret on every deploy, so the laptop rotation procedure is: ```bash NEW_TOKEN=$(openssl rand -hex 32) npm run sst -- secret set McpAuthToken "$NEW_TOKEN" gh secret set MCP_AUTH_TOKEN --body "$NEW_TOKEN" # Set MCP_AUTH_TOKEN to the same value in ~/.config/vault-cortex/.env. -# Then dispatch manual_release.yml or push a new tag — CI takes care of the rest. +# Then dispatch manual_release.yml — CI takes care of the rest. ``` ### Don't fork-deploy without re-staging diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 720167b1a..f0c1bb75b 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -52,8 +52,9 @@ Everything needed to continue the project is public: - Source, CI workflows, tests, and documentation live in this repository - The [MIT license](./LICENSE) permits anyone to fork and continue development -- Releases are built by GitHub Actions from tagged commits — there are no - maintainer-local build steps +- Releases are cut by GitHub Actions workflows (Manual Release for the + server, Release CLI for the npm package), which tag a commit and build + from it — there are no maintainer-local build steps A successor or fork would need to re-create the private pieces: the deploy workflows' secrets (deployment credentials, the Docker Hub diff --git a/SECURITY.md b/SECURITY.md index ca407eb2f..bbc83bcb5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -338,7 +338,7 @@ gh release download v0.38.0 --pattern 'digests.txt*' cosign verify-blob \ --bundle digests.txt.sigstore.json \ - --certificate-identity-regexp '^https://github\.com/aliasunder/vault-cortex/\.github/workflows/(auto_release|manual_release)\.yml@.*$' \ + --certificate-identity-regexp '^https://github\.com/aliasunder/vault-cortex/\.github/workflows/manual_release\.yml@.*$' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ digests.txt ``` @@ -347,7 +347,8 @@ Then confirm the image you pulled matches a signed digest: ```bash docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/aliasunder/vault-cortex:v0.38.0 -# Compare the sha256:... with the corresponding line in digests.txt +# Compare the sha256:... with the digests.txt line for the same tag: +# :v0.38.0 is the local target, :v0.38.0-remote the remote target ``` ## Reporting a Vulnerability