Skip to content

feat: add an action to notify Pubgrade from CI - #115

Merged
uniqueg merged 5 commits into
devfrom
feat/88-notify-action
Nov 13, 2026
Merged

uniqueg merged 5 commits into
devfrom
feat/88-notify-action

Conversation

@uniqueg

@uniqueg uniqueg commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds the composite action actions/notify, which notifies Pubgrade of a new image from CI: it registers a prebuilt image as a build of a repository (POST /repositories/{id}/builds), and Pubgrade then notifies the repository's subscriptions. Callers reference it as elixir-cloud-aai/Pubgrade/actions/notify@<commit> # <version>, as decided in #88.

  • Inputs: url, repository-id, token and image (required); build-type, git-tag, git-branch, git-commit, fail-on-error (default true) and allow-http (default false). Output: build-id.
  • Credentials: the caller passes the URL, the ID and the token as inputs, with the token from a secret. The action masks the token first, never prints it, and passes it to curl through stdin, not on the command line. It accepts only HTTPS unless allow-http is set. All inputs reach the script through env, never interpolated into it, and the messages it prints are escaped for the runner.
  • Validation: inputs the action checks itself always fail the step if invalid, whatever fail-on-error says (required inputs, a URL curl cannot parse, the formats of the ID, the token and the image, git-commit in a tag run). The ID and the token must use the characters Pubgrade generates (pubgrade/charsets.py), and the image needs a tag.
  • Retries: only requests that never left the runner are retried (the connection is refused or the host name does not resolve), at most 3 attempts. No answer is retried, not even a 502, 503 or 504 of a proxy, because Pubgrade may have registered the build already (e.g. a proxy times out while Pubgrade notifies the subscriptions).
  • head_commit: taken from the run's tag, or else from its branch and commit, unless the git-* inputs set it. In a run for a tag, git-commit needs git-branch.
  • Difference from the issue: Pubgrade accepts only an image with a tag, not a digest (IMAGE_WITH_TAG in builds.py), so image takes name:tag.
  • CI: a new job notify / "Notify action" runs the action, as callers do, against Pubgrade built from the branch's image. It covers a registered build, a wrong token that fails, and a wrong token with fail-on-error: false that only warns. CI passed and publish need the new job. The app start moves from the smoke job to tests/start.sh, which both jobs use.
  • Docs: a "Notify Pubgrade from CI" section in docs/usage.md (usage, inputs, retries, runs for pull requests), and notes in docs/development.md (tests, CI).

The action needs Pubgrade with builds switched off (the default). Adoption in tesk-api is tracked in elixir-cloud-aai/tesk-api#157 and needs a Pubgrade release first.

Verification

  • pytest --cov: 756 passed, 100% coverage (41 of the tests are in tests/test_notify_action.py, which runs the script against a stub of Pubgrade).
  • pre-commit run --all-files passed: ruff, mypy, actionlint, gitleaks and the file checks.
  • mkdocs build --strict passed.
  • The first version of notify.sh ran against a real Pubgrade started with tests/start.sh (local image build):
    • The build was registered with status SUCCEEDED, the image and the branch and commit as sent.
    • A wrong token gave ::error::Pubgrade answered 401: Unauthorized: … and a failed step.
    • The same with fail-on-error=false gave a warning and exit 0.
    • An undeclared build type gave a 400 with Pubgrade's detail.
    • The CI job notify repeats these checks with the final version.
  • CI: all checks pass, including "Notify action". On the first run, "Deployment check" failed once (Pubgrade's notification of the sidecar in the k3s cluster, untouched by this PR); it passed on the same commit in the other run and on the rerun.
  • Reviews: adversarial-reviewer and quality-reviewer ran on the branch. The fixed findings were 502/504 retries, message escaping, git-commit in tag runs, test gaps, the job id, CI check messages, wrapping and docs. A second adversarial round confirmed these fixes; its low findings are fixed too: each line of the token is masked, the retried failures are documented precisely (incl. the remaining risk of 503s from proxies like Envoy), and the git-branch description is corrected.
  • Sourcery: of its 9 threads, the following were fixed in 35f3e66: no retry of 503s either, _ rejected in IDs and tokens, unparsable URLs fail, builds-off requirement and checked inputs documented. The others were answered with reasons (see below); all threads are resolved.

Declined review findings

  • Move the CI job's steps into a tests/notify.sh script (quality, optional): the job runs the action with uses:, as callers do, which a script can't do. Its checks now print error messages instead.
  • Shorten the input descriptions in action.yml to avoid duplicating the docs (quality, low): GitHub and the Marketplace show these descriptions on their own, so they stay complete.
  • Retry on all 5xx (Sourcery, from the issue): the request isn't idempotent, so retries could register builds twice (see "Retries" above).
  • Digest in image (Sourcery, from the issue): Pubgrade's API takes tags only.
  • Validate the full image reference locally (Sourcery): Pubgrade checks it and names the problem; a Bash copy of its rules would drift.
  • Fail regardless of fail-on-error when Pubgrade rejects the request (Sourcery): fail-on-error: false is meant to let a release go on whatever Pubgrade answers; the step still warns.
  • Order notifications for each repository or ref (Sourcery): a matter for Pubgrade's API, not this client.

Open questions

None.

Closes #88

🤖 Generated with Claude Code

uniqueg and others added 4 commits November 13, 2026 07:41
Add the composite action actions/notify, which registers a prebuilt
image as a build of a repository (POST /repositories/{id}/builds), so
that other repositories can notify Pubgrade from their release
workflows with a few lines of YAML instead of an inline step. Callers
pass the URL, the repository ID and the token as inputs (the token from
a secret); the action masks the token, sends it in the header via
curl's stdin only, takes HTTPS only unless allow-http is set, derives
head_commit from the run's tag or branch and commit unless given, and
outputs the build ID. Invalid inputs always fail; failed requests fail
unless fail-on-error is false. Only requests that didn't reach Pubgrade
(no connection, 502, 503, 504) are retried, so that a build is never
registered twice.

Pubgrade takes an image with a tag and no digest, so the input image
takes no digest either (the issue preferred one).

Test the script against a stub of Pubgrade (tests/test_notify_action.py)
and the action itself in the new CI job "Notify action" against
Pubgrade from the branch's image, which the gate now needs. The start
of the app moves from the smoke job to tests/start.sh, shared by both
jobs. Document the action in the usage and development docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repository and build identifiers may contain '.' and '~' (the
characters that pubgrade/charsets.py allows), which the action
rejected. Check the token against the same characters, so that it
can't break out of the quotes in curl's configuration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 502 or 504 may come after Pubgrade registered the build (e.g. a
proxy's timeout while Pubgrade notifies the subscriptions), so the
action no longer retries them; it still retries when it can't connect
and on a 503. Escape the messages of workflow commands, so that
Pubgrade's error text stays one annotation. In a run for a tag,
git-commit now needs git-branch, instead of sending the tag as branch.

Test the warning paths, error bodies without JSON or detail, and curl
errors that aren't retried; rename the CI job to notify, give its
checks error messages, document pull request runs, and rewrap the
comments and docs that the first commit left too long.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mask every line of the token before checking it, as the runner masks
secrets, so that an invalid token with line breaks isn't printed.
Document which failures are retried (refused connections, unresolved
host names, 503) and that proxies like Envoy may answer 503 after
Pubgrade received the request. Fix the default of git-branch in the
action's description, and show the body when the CI job's build check
can't fetch the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

API

Define the composite action’s inputs, output, and invocation contract for callers registering a prebuilt image with Pubgrade.

  • actions/notify/action.yml L1-92

Uses of the API

Exercise the composite action against a live branch-built Pubgrade instance and make notification checks prerequisites for publishing and the CI gate.

  • .github/workflows/ci.yml L4-12, L256-355 (+4 small edits)

Implementation

Implement input validation, Git metadata selection, authenticated build registration, retry behavior, and workflow outputs for the notify action.

  • actions/notify/notify.sh L1-173

Extract shared Docker startup and readiness logic for running Pubgrade with MongoDB in CI.

  • tests/start.sh L1-29

Documentation

Document how callers configure the action and its inputs, outputs, retry behavior, and development and CI coverage.

  • docs/development.md L53-64, L121-125
  • docs/usage.md L104-155

Tests

Test the notify script’s request payload, credential handling, validation, Git metadata, errors, and retry behavior against a local stub.

  • tests/test_notify_action.py L1-431

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@read-the-docs-community

read-the-docs-community Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 Pubgrade | 🛠️ Build #35869594 | 📁 Comparing 35f3e66 against latest (646c862)

  🔍 Preview build  

2 files changed
± development/index.html
± usage/index.html

@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (646c862) to head (35f3e66).

Additional details and impacted files
@@            Coverage Diff            @@
##               dev      #115   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           18        18           
  Lines         1545      1545           
=========================================
  Hits          1545      1545           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@uniqueg
uniqueg marked this pull request as ready for review November 13, 2026 13:22
@sourcery-ai

sourcery-ai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Linked issues

Checked at commit 35f3e66

#88 feat: reusable action to notify Pubgrade from CI · 7 of 9 met

# Requirement Status Notes
1 Implement the action under actions/notify as a composite action usable as a step in the caller's workflow. ✅ Met actions/notify/action.yml defines a composite action.
2 Provide the required url, repository-id, token and image inputs; optional build-type and fail-on-error; and the build-id output. ✅ Met The action also provides the git metadata and allow-http inputs.
3 Accept an image reference, preferably one with a digest. 🟡 Partly met The action requires a tag and rejects any digest.
4 Post to the repository builds endpoint, mask the token, and use HTTPS unless allow-http is enabled. ✅ Met The script sends the token in curl's configuration through stdin and restricts protocols based on allow-http.
5 Retry briefly on 5xx responses. 🟡 Partly met The action retries connection failures but does not retry HTTP 5xx responses.
6 Fail with a clear message when a required input is missing. ✅ Met Required inputs are checked before the request and produce an error annotation.
7 Keep the action's execution conditional under the caller's control. ✅ Met The composite action defines no condition of its own.
8 Test the action with unit tests and an integration against Pubgrade. ✅ Met The script is tested against a stub, and the CI notify job runs the action against a locally started Pubgrade instance.
9 Document how to use the action in Pubgrade's usage documentation. ✅ Met docs/usage.md includes an example, inputs, output, and retry behavior.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 9 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="actions/notify/notify.sh" line_range="142" />
<code_context>
+    0:200)
+      break
+      ;;
+    6:* | 7:* | 0:503)
+      if [[ "${status}" -eq 0 ]]; then
+        reason="Pubgrade answered ${code}"
</code_context>
<issue_to_address>
**Duplicate builds and notifications**

When a proxy returns 503 after Pubgrade has accepted and registered the POST, `notify.sh` retries the non-idempotent POST, and each `register_builds` call creates a fresh build ID and notifies subscribers again. The repository gains duplicate builds and deployments receive duplicate updates.

Make the POST idempotent or avoid retrying when Pubgrade may already have accepted it.
</issue_to_address>

### Comment 2
<location path="actions/notify/notify.sh" line_range="133" />
<code_context>
+  status=0
+  code="$(curl --proto "${protocols}" --silent --show-error \
+    --connect-timeout 10 --max-time 60 \
+    --request POST "${url%/}/repositories/${repository_id}/builds" \
+    --header 'Content-Type: application/json' \
+    --data "${body}" \
</code_context>
<issue_to_address>
**Older builds can overwrite newer deployments**

When two CI runs publish images for the same subscribed ref concurrently and the older notification reaches the subscriber last, pubgrade handles the two POSTs independently and can deliver the older image after the newer one; subscribers receive only the image name and tag, with no commit or sequence to reject a stale notification, so a deployment ends on the older image.

Serialize notifications for each repository/ref, or include a monotonic version that subscribers can use to reject stale notifications.
</issue_to_address>

### Comment 3
<location path="actions/notify/notify.sh" line_range="113-114" />
<code_context>
+body="$(jq --null-input --compact-output \
+  --arg name "${image}" --arg type "${build_type}" \
+  --argjson head "${head_commit}" \
+  '{images: [{name: $name}], head_commit: $head}
+   + if $type == "" then {} else {build_type: $type} end')"
+
+response="$(mktemp)"
</code_context>
<issue_to_address>
**Action fails when Pubgrade builds images**

When pubgrade is configured with `endpoints.builds.enabled` set to `true`, `register_builds` uses its native-build path when Pubgrade builds are enabled, and `start_build` then requires `location` and `dockerhub_token`; this action sends neither, so Pubgrade rejects the request and the caller does not register the prebuilt image.

Document and enforce that the action requires Pubgrade builds to be disabled, or add API support for registering prebuilt images when builds are enabled.
</issue_to_address>

### Comment 4
<location path="actions/notify/notify.sh" line_range="159" />
<code_context>
+      # Pubgrade's errors are problem details (`title`, `detail`)
+      detail="$(jq -r '[.title, .detail] | map(select(. != null)) | join(": ")' \
+        "${response}" 2> /dev/null | head -c 500 || true)"
+      give_up "Pubgrade answered ${code}${detail:+: ${detail}}"
+      ;;
+    *)
</code_context>
<issue_to_address>
**Invalid requests can succeed**

When `fail-on-error` is `false` and Pubgrade rejects a malformed image, undeclared build type, or other invalid request field, `give_up` emits a warning and exits 0 for Pubgrade rejections, so invalid values the script leaves to Pubgrade—such as a malformed image or undeclared build type—make the action succeed without registering a build, despite its promise that invalid inputs always fail.

Validate request fields locally or make known validation rejections fail regardless of `fail-on-error`.
</issue_to_address>

### Comment 5
<location path="actions/notify/notify.sh" line_range="72" />
<code_context>
+# so it starts with a letter or digit (no `.` or `..`); the token goes into
+# curl's configuration in quotes, so it has no quotes, backslashes or line
+# breaks (and isn't printed)
+if [[ ! "${repository_id}" =~ ^[A-Za-z0-9][A-Za-z0-9._~-]*$ ]]; then
+  fail "Invalid input 'repository-id': letters, digits, '.', '_', '~' and '-' only, starting with a letter or digit"
+fi
</code_context>
<issue_to_address>
**Impossible identifiers are accepted**

When a caller supplies an uppercase letter, `_` or `~` in a repository ID, or `_` in a token, and sets `fail-on-error` to `false`, the local regexes accept characters Pubgrade cannot generate for these values, so the request reaches Pubgrade and is rejected. `give_up` exits successfully, and the workflow reports success without registering a build.

Restrict the repository ID and token regexes to the characters Pubgrade can generate for each value.

Also at `actions/notify/notify.sh:75`.
</issue_to_address>

### Comment 6
<location path="actions/notify/notify.sh" line_range="80" />
<code_context>
+fi
+# Pubgrade notifies subscribers of the image's tag, so it needs a tag and
+# takes no digest; it checks the full reference itself
+if [[ "${image}" == *@* || ! "${image##*/}" =~ :. ]]; then
+  fail "Input 'image' needs a tag and no digest, e.g. 'org/image:1.0.0'"
+fi
</code_context>
<issue_to_address>
**Invalid tagged images succeed**

When `fail-on-error` is `false` and an image has a tag-shaped suffix but fails Pubgrade’s image-reference validation, the shallow check accepts the image, Pubgrade rejects it, and `give_up` exits successfully with a warning. The caller sees a successful step for an invalid image input.

Validate the full image reference locally before sending the request.
</issue_to_address>

### Comment 7
<location path="actions/notify/notify.sh" line_range="89-90" />
<code_context>
+    || fail "Input 'url' must start with https:// or http://"
+else
+  protocols="=https"
+  [[ "${url}" =~ ^https:// ]] \
+    || fail "Input 'url' must start with https:// (or set 'allow-http')"
+fi
+
</code_context>
<issue_to_address>
**Malformed URLs only warn**

When `fail-on-error` is `false` and `url` has the required scheme but is otherwise malformed, the scheme check accepts malformed values such as `https://bad host`, so `curl` rejects the request and `give_up` exits successfully with a warning when `fail-on-error` is false; the caller sees an invalid input treated as success.

Validate that `url` is a well-formed base URL before making the request, and fail that validation regardless of `fail-on-error`.
</issue_to_address>

### Comment 8
<location path="actions/notify/notify.sh" line_range="142" />
<code_context>
+    0:200)
+      break
+      ;;
+    6:* | 7:* | 0:503)
+      if [[ "${status}" -eq 0 ]]; then
+        reason="Pubgrade answered ${code}"
</code_context>
<issue_to_address>
**Other 5xx retries**

The issue asks for retries on 5xx responses, but the action retries only 503 responses (as well as connection failures). Other 5xx responses are not retried.
</issue_to_address>

### Comment 9
<location path="actions/notify/notify.sh" line_range="80-81" />
<code_context>
+fi
+# Pubgrade notifies subscribers of the image's tag, so it needs a tag and
+# takes no digest; it checks the full reference itself
+if [[ "${image}" == *@* || ! "${image##*/}" =~ :. ]]; then
+  fail "Input 'image' needs a tag and no digest, e.g. 'org/image:1.0.0'"
+fi
+if [[ "${allow_http}" == "true" ]]; then
</code_context>
<issue_to_address>
**Digest image references**

The issue prefers an image input with a digest, but this validation rejects digests and requires a tag. The PR explains that Pubgrade currently accepts only tagged images, so this request remains unmet.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 9 findings to address first, and if the action sends the wrong image or Git reference, Pubgrade can persist that build and notify subscribed deployments; those notifications or any resulting rollout cannot be undone by reverting this change. The action also sends a repository access token in an authenticated request, though it masks the token and avoids placing it on the command line.

Blocking findings: actions/notify/notify.sh:142, actions/notify/notify.sh:133, actions/notify/notify.sh:114, actions/notify/notify.sh:159, actions/notify/notify.sh:72, and 4 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
Comment thread actions/notify/notify.sh Resolved
A 503 from a proxy may also come after Pubgrade received the request
(e.g. Envoy on an upstream reset), so the action retries only when it
can't connect at all. Reject '_' in repository IDs and tokens, which
Pubgrade never generates, and fail on URLs that curl can't parse,
whatever fail-on-error says. Document that the action needs builds
switched off, and which inputs it checks itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New gaps against the linked issues

Comment thread actions/notify/notify.sh Resolved
@uniqueg
uniqueg merged commit 0694d03 into dev Nov 13, 2026
80 of 82 checks passed
@uniqueg
uniqueg deleted the feat/88-notify-action branch November 13, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: reusable action to notify Pubgrade from CI

1 participant