Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/publish_flutter_solidart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ name: Publish flutter_solidart to pub.dev
on:
push:
tags:
- 'flutter_solidart-v[0-9]+.[0-9]+.[0-9]+' # Matches flutter_solidart-v1.2.3
- 'flutter_solidart-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches flutter_solidart-v1.2.3+1
- "flutter_solidart-v[0-9]+.[0-9]+.[0-9]+*" # Matches flutter_solidart-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files .github/workflows scripts | sed -n '1,200p'

Repository: nank1ro/solidart

Length of output: 439


🏁 Script executed:

sed -n '1,220p' .github/workflows/publish_flutter_solidart.yaml
printf '\n---\n'
sed -n '1,220p' .github/workflows/publish_solidart.yaml
printf '\n---\n'
sed -n '1,220p' .github/workflows/publish_solidart_hooks.yaml
printf '\n---\n'
sed -n '1,220p' .github/workflows/publish_solidart_lint.yaml
printf '\n---\n'
sed -n '1,220p' scripts/release.sh

Repository: nank1ro/solidart

Length of output: 7528


🌐 Web query:

GitHub Actions workflow syntax push tags glob pattern square brackets plus literal semver tag matching

💡 Result:

GitHub Actions workflow on.push.tags filters support glob patterns, not regular expressions [1]. While glob syntax includes some special characters, it does not support full regex character classes or quantifier-based pattern matching (such as [0-9]+) [1]. Key technical details regarding your query include: 1. Glob vs. Regex: The tags filter uses glob patterns (e.g., *, ``, ?, `!`) [1][4][8]. Attempts to use regex-style syntax like `[0-9]+` will not function as regex; GitHub Actions will interpret these literally or fail to match them against the tag string [1]. 2. Square Brackets in YAML: Square brackets `[` and `]` are special characters in YAML [2]. If your tag pattern starts with `[` or contains them in a way that YAML interprets as a flow sequence, you must enclose the entire pattern in quotes (e.g., `'[pattern]'`) to ensure it is treated as a string literal [2][3]. 3. Literal Matching: If a tag name happens to contain glob characters (like ``, `?`, or `+`) and you want to match that tag literally, you must escape those characters with a backslash `` [4][5][6]. 4. SemVer Matching: Because GitHub Actions does not support regex, there is no native way to perform complex SemVer range matching (e.g., "all tags between 1.0.0 and 2.0.0") directly in the workflow trigger. Common strategies include using broad glob patterns like `v1.` or `v*` and then using a conditional step within the workflow (e.g., `if: ${{ startsWith(github.ref_name, 'v1.') }}`) or a dedicated action to validate the version [7][1]. In summary, you cannot use regex for tag filtering. You should rely on glob patterns for broad inclusion/exclusion and handle specific version logic inside the workflow job itself.

Citations:


Fix the tag filter syntax GitHub Actions treats tags: as globs, not regex, so this pattern does not match the <name>-v<version> tags emitted by scripts/release.sh. Use a valid glob and validate the version in the job if semver constraints are needed. The same pattern is duplicated in the sibling publish workflows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish_flutter_solidart.yaml at line 6, The tag filter in
the publish workflow is using regex-like syntax instead of a GitHub Actions
glob, so it will not match the release tags emitted by scripts/release.sh.
Update the tags pattern in this workflow to a valid glob that matches the
<name>-v<version> tag shape, and apply the same change to the sibling publish
workflows that duplicate this filter. If semver validation is still required,
enforce it inside the job rather than in the trigger pattern.


# Publish using the reusable workflow from dart-lang.
jobs:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/publish_solidart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ name: Publish solidart to pub.dev
on:
push:
tags:
- "solidart-v[0-9]+.[0-9]+.[0-9]+" # Matches solidart-v1.2.3
- 'solidart-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches solidart-v1.2.3+1
- "solidart-v[0-9]+.[0-9]+.[0-9]+*" # Matches solidart-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease)

jobs:
publish:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/publish_solidart_hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ name: Publish solidart_hooks to pub.dev
on:
push:
tags:
- 'solidart_hooks-v[0-9]+.[0-9]+.[0-9]+' # Matches solidart_hooks-v1.2.3
- 'solidart_hooks-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches solidart_hooks-v1.2.3+1
- "solidart_hooks-v[0-9]+.[0-9]+.[0-9]+*" # Matches solidart_hooks-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease)

# Publish using the reusable workflow from dart-lang.
jobs:
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/publish_solidart_lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ name: Publish solidart_lint to pub.dev
on:
push:
tags:
- 'solidart_lint-v[0-9]+.[0-9]+.[0-9]+' # Matches solidart_lint-v1.2.3
- 'solidart_lint-v[0-9]+.[0-9]+.[0-9]+\+[0-9]+' # Matches solidart_lint-v1.2.3+1
- "solidart_lint-v[0-9]+.[0-9]+.[0-9]+*" # Matches solidart_lint-v1.2.3, -v1.2.3+1 (build), -v1.2.3-dev.1 (prerelease)

# Publish using the reusable workflow from dart-lang.
jobs:
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Release to pub.dev (orchestrated)

# One-click release: publishes every package whose pubspec version is not yet on
# pub.dev, in dependency order, waiting for each to become available before
# releasing its dependents. Already-published versions are skipped.
#
# Requires the `RELEASE_PAT` repo secret (a fine-grained PAT with Contents: write,
# or a GitHub App token). Tags pushed by the default GITHUB_TOKEN do NOT trigger
# the publish_*.yaml workflows, so a separate identity is required.

on:
workflow_dispatch:
inputs:
dry_run:
description: "Log the planned releases without creating tags/releases"
type: boolean
default: false

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history + tags

- name: Orchestrate pub.dev releases
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
DRY_RUN: ${{ inputs.dry_run }}
run: bash scripts/release.sh
140 changes: 140 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
#
# Orchestrates pub.dev publishing for the solidart monorepo.
#
# For every publishable package, in dependency order, this script:
# 1. reads the `version:` from its pubspec.yaml,
# 2. skips it if that version is already on pub.dev (no-op for unchanged packages),
# 3. otherwise creates a GitHub Release + tag (`<name>-v<version>`), which triggers
# the package's existing `publish_<name>.yaml` workflow,
# 4. waits until pub.dev actually serves the new version before moving on to the
# packages that depend on it.
#
# It does NOT publish directly and it does NOT bump versions — version bumps and
# CHANGELOG edits stay manual. The pubspec `version:` is the source of truth.
#
# Requires: gh, curl, jq, git (all preinstalled on ubuntu-latest).
# Environment:
# GH_TOKEN a non-GITHUB_TOKEN identity (PAT / App token) so the tag push it
# creates triggers the downstream publish_*.yaml workflows.
# GITHUB_SHA the commit to tag (auto-set by GitHub Actions).
# DRY_RUN "true" to log the plan without creating any tags/releases.
#
set -euo pipefail

# Publishable packages in topological order. A package is only released after
# every in-repo dependency it has is already live on pub.dev.
# solidart -> (no in-repo deps)
# flutter_solidart -> solidart
# solidart_hooks -> flutter_solidart
# solidart_lint -> solidart, flutter_solidart (dev_dependencies)
PACKAGES=(
"solidart:packages/solidart"
"flutter_solidart:packages/flutter_solidart"
"solidart_hooks:packages/solidart_hooks"
"solidart_lint:packages/solidart_lint"
)

DRY_RUN="${DRY_RUN:-false}"
PUB_API="https://pub.dev/api/packages"
POLL_ATTEMPTS=60 # 60 * 30s = up to 30 min, to absorb pub.dev propagation lag
POLL_INTERVAL=30

# Read the package version from its pubspec (first `version:` line, value only).
read_version() {
awk '/^version:/{print $2; exit}' "$1/pubspec.yaml"
}

# True when <version> ($2) is already published for package <name> ($1).
# A cache-busting query + no-cache header avoid a stale CDN response masking a
# just-published version. Network/HTTP errors are treated as "not published".
is_published() {
local body
body="$(curl -fsS -H 'Cache-Control: no-cache' "${PUB_API}/$1?_=${RANDOM}" 2>/dev/null || true)"
[ -n "$body" ] || return 1
printf '%s' "$body" | jq -e --arg v "$2" '.versions[]?.version | select(. == $v)' >/dev/null 2>&1
Comment on lines +48 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Fail closed when the pub.dev lookup errors.

Treating curl/HTTP failures as “not published” breaks the workflow’s main safety guarantee. A transient pub.dev outage can make this script create a new release/tag for a version that is already live, which then triggers a downstream publish that can only fail. Return a distinct status for lookup failures and abort before gh release create; only the polling path should treat lookup errors as retryable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/release.sh` around lines 48 - 55, The is_published helper currently
swallows curl/HTTP failures and returns “not published,” which can let release
tagging proceed during pub.dev outages. Update is_published to return a distinct
non-published/non-error status for successful lookups only, and make the caller
path that reaches gh release create abort on lookup failures while keeping the
polling logic retryable. Use is_published, curl, and the gh release create flow
in scripts/release.sh to locate the change.

}

# True when the tag <tag> ($1) already exists on the remote.
tag_exists() {
[ -n "$(git ls-remote --tags origin "refs/tags/$1")" ]
}

# Print the CHANGELOG section for <version> ($2) from <dir>/CHANGELOG.md ($1):
# everything between the `## <version>` heading and the next `## ` heading.
# Uses a literal heading compare to avoid regex-escaping the version string.
changelog_notes() {
local file="$1/CHANGELOG.md"
[ -f "$file" ] || return 0
awk -v h="## $2" '
{ line = $0; sub(/[[:space:]]+$/, "", line) }
line == h { f = 1; next }
/^## / { if (f) exit }
f { print }
' "$file"
}

# Block until <version> ($2) of <name> ($1) is live on pub.dev, or fail.
wait_for_publish() {
local name="$1" version="$2" i
for ((i = 1; i <= POLL_ATTEMPTS; i++)); do
if is_published "$name" "$version"; then
return 0
fi
echo " …not on pub.dev yet (attempt ${i}/${POLL_ATTEMPTS}); sleeping ${POLL_INTERVAL}s"
sleep "$POLL_INTERVAL"
done
echo " ✗ timed out after $((POLL_ATTEMPTS * POLL_INTERVAL / 60)) min waiting for ${name} ${version} on pub.dev" >&2
return 1
}

[ "$DRY_RUN" = "true" ] && echo "DRY RUN — no tags or releases will be created."

for entry in "${PACKAGES[@]}"; do
name="${entry%%:*}"
dir="${entry#*:}"
version="$(read_version "$dir")"

if [ -z "$version" ]; then
echo "✗ could not read version from ${dir}/pubspec.yaml" >&2
exit 1
fi

tag="${name}-v${version}"
echo "── ${name} ${version} (tag ${tag})"

if is_published "$name" "$version"; then
echo " ✓ already on pub.dev — skipping"
continue
fi

if tag_exists "$tag"; then
echo " ✗ tag ${tag} exists but ${version} is not on pub.dev — a previous publish likely failed."
echo " Re-run the failed '${name}' publish workflow, or delete the tag and re-run this workflow."
if [ "$DRY_RUN" = "true" ]; then
echo " [dry-run] continuing"
continue
fi
exit 1
fi

if [ "$DRY_RUN" = "true" ]; then
echo " [dry-run] would create release + tag ${tag}, then wait for pub.dev"
continue
fi

notes="$(changelog_notes "$dir" "$version")"

echo " → creating GitHub release + tag ${tag}"
release_args=("$tag" --title "$tag" --notes "${notes:-Release $tag}" --target "${GITHUB_SHA}")
case "$version" in
*-*) release_args+=(--prerelease) ;; # e.g. 3.0.0-dev.1
esac
gh release create "${release_args[@]}"

echo " → waiting for ${name} ${version} to appear on pub.dev"
wait_for_publish "$name" "$version"
echo " ✓ ${name} ${version} is live on pub.dev"
done

echo "Done."
Loading