feat: add reusable publish-preview workflow#223
Conversation
Extract the preview build preparation logic into a reusable composite GitHub Action so other repos can use the same logic. The jq filter is inlined and the source scope is parameterized.
|
@cryptodev-2s What are your thoughts on supporting the entire preview build workflow rather than just the "generate preview builds" step? Recently, we've added support for preview builds to other polyrepos, but currently we have to copy around the workflows. It would be nice to consolidate all of the steps into this repo. And we could make the action support both monorepos and polyrepos. What do you think? |
Yes make sense, let me do this. |
Add a `workflow_call` workflow that handles the full preview build flow (fork check, comment reaction, build, publish) for both monorepos and polyrepos. Consumer repos only need a ~13-line thin wrapper with the `issue_comment` trigger. Also add a centralized `generate-preview-build-message.sh` script that auto-detects mono vs polyrepo and produces the appropriate PR comment.
|
|
||
| - name: Prepare preview builds (monorepo) | ||
| if: ${{ inputs.is-monorepo }} | ||
| uses: MetaMask/github-tools/.github/actions/prepare-preview-builds@prepare-preview-builds-action |
There was a problem hiding this comment.
Hardcoded branch ref will break after PR merge
High Severity
The uses: reference at @prepare-preview-builds-action points to what appears to be this PR's development branch. Once the PR is merged and the branch is deleted, the monorepo build step will fail for every consumer of this reusable workflow. The publish job correctly resolves the ref dynamically from github.workflow_ref, but the build job hardcodes this ephemeral branch name instead.
Additional Locations (1)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
| !./packages/*/node_modules/ | ||
| !./packages/*/src/ | ||
| !./packages/*/tests/ | ||
| !./packages/**/*.test.* |
There was a problem hiding this comment.
Monorepo artifact paths hardcode packages/ directory structure
Medium Severity
The monorepo upload glob ./packages/*/ and the sanitization find packages both hardcode the packages/ directory. However, prepare-preview-builds.sh dynamically discovers workspaces via yarn workspaces list, which works for any directory layout. Monorepos with workspaces outside packages/ (e.g., libs/, modules/) would have their manifests prepared but the built output wouldn't be uploaded or security-validated before publishing.
Additional Locations (1)
There was a problem hiding this comment.
All MetaMask monorepos use packages/, this matches the existing core workflow convention. If a repo ever uses a different layout, we can add a workspace-glob input then.
There was a problem hiding this comment.
We could generate this glob automatically by looping through the workspaces field in package.json. But yes I agree that probably don't need to worry about this for v1.
There was a problem hiding this comment.
What do you think about turning this into an action and then merging the prepare-preview-builds action into it?
(I have made suggestions below as if the workflow is not being converted, FYI)
| - name: Sanitize and validate artifact manifests | ||
| run: | | ||
| bad=0 | ||
| if [[ "${{ inputs.is-monorepo }}" == "true" ]]; then |
There was a problem hiding this comment.
Instead of injecting this should we set it in an environment variable and then read the variable?
| # Build a JSON object of { name: version } for all non-root workspace packages. | ||
| package_json="$( | ||
| yarn workspaces list --no-private --json \ | ||
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ |
There was a problem hiding this comment.
Do we need to omit .? I thought --no-private should take care of that:
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ | |
| | jq --slurp --raw-output '.[].location' \ |
| cat > preview-build-message.txt <<MSGEOF | ||
| Preview builds have been published.${docs_link} | ||
|
|
||
| \`\`\` | ||
| yarn add ${name}@${version} | ||
| \`\`\` | ||
| MSGEOF |
There was a problem hiding this comment.
Wondering if we should say:
| cat > preview-build-message.txt <<MSGEOF | |
| Preview builds have been published.${docs_link} | |
| \`\`\` | |
| yarn add ${name}@${version} | |
| \`\`\` | |
| MSGEOF | |
| cat <<-MSGEOF > preview-build-message.txt | |
| The following preview build has been published: | |
| \`\`\` | |
| ${name}@${version} | |
| \`\`\` | |
| MSGEOF | |
| if [[ -n $docs_link ]]; then | |
| cat <<-MSGEOF >> preview-build-message.txt | |
| ${docs_link} | |
| MSGEOF | |
| fi |
| cat > preview-build-message.txt <<MSGEOF | ||
| Preview builds have been published.${docs_link} | ||
|
|
||
| <details> | ||
|
|
||
| <summary>Expand for full list of packages and versions.</summary> | ||
|
|
||
| \`\`\` | ||
| ${package_json} | ||
| \`\`\` | ||
|
|
||
| </details> | ||
| MSGEOF |
There was a problem hiding this comment.
Wondering if we should say:
| cat > preview-build-message.txt <<MSGEOF | |
| Preview builds have been published.${docs_link} | |
| <details> | |
| <summary>Expand for full list of packages and versions.</summary> | |
| \`\`\` | |
| ${package_json} | |
| \`\`\` | |
| </details> | |
| MSGEOF | |
| cat <<-MSGEOF > preview-build-message.txt | |
| Preview builds have been published. | |
| MSGEOF | |
| if [[ -n $docs_link ]]; then | |
| echo -n " ${docs_link}" >> preview-build-message.txt | |
| fi | |
| cat <<-MSGEOF >> preview-build-message.txt | |
| <details> | |
| <summary>Expand for full list of packages and versions.</summary> | |
| \`\`\` | |
| ${package_json} | |
| \`\`\` | |
| </details> | |
| MSGEOF |
| while IFS=$'\t' read -r location name; do | ||
| echo "- $name" | ||
| prepare-preview-manifest "$location/package.json" | ||
| done < <(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map(select(.location != ".")) | map([.location, .name]) | map(@tsv) | .[]') |
There was a problem hiding this comment.
I know that the script in core does this, but now that I look at this, do we need to exclude . here either? --no-private should take care of this as well.
| done < <(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map(select(.location != ".")) | map([.location, .name]) | map(@tsv) | .[]') | |
| done < <(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map([.location, .name]) | map(@tsv) | .[]') |
| # Build a JSON object of { name: version } for all non-root workspace packages. | ||
| package_json="$( | ||
| yarn workspaces list --no-private --json \ | ||
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ | ||
| | while read -r location; do | ||
| jq '{ (.name): .version }' "$location/package.json" | ||
| done \ | ||
| | jq --slurp 'add' | ||
| )" |
There was a problem hiding this comment.
A few things:
-
Instead of building an object here what do you think about listing out the names and versions? The current way that we output the list of versions makes it difficult to update a project's
package.jsonmanually. -
Do we need to omit
.?I thought--no-privateshould take care of that. -
We can use
xargsinstead of a loop. There is a script incorethat does something very similar: https://github.com/MetaMask/core/blob/416032dbbed5c2bf1340572964a09c91d6e06dc7/scripts/list-workspace-versions.sh
| # Build a JSON object of { name: version } for all non-root workspace packages. | |
| package_json="$( | |
| yarn workspaces list --no-private --json \ | |
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ | |
| | while read -r location; do | |
| jq '{ (.name): .version }' "$location/package.json" | |
| done \ | |
| | jq --slurp 'add' | |
| )" | |
| # Build a list of "<name>@<version>" for all non-root workspace packages. | |
| packages="$( | |
| yarn workspaces list --no-private --json | \ | |
| jq --raw-output '.location' | \ | |
| xargs -I{} cat '{}/package.json' | \ | |
| jq --raw-output '"\(.name)@\(.version)"' | |
| )" |
| - name: Generate preview build message | ||
| run: | | ||
| args=() | ||
| if [[ "${{ inputs.is-monorepo }}" == "true" ]]; then |
There was a problem hiding this comment.
Should we use environment variables instead of reading inputs directly?
| !./packages/*/node_modules/ | ||
| !./packages/*/src/ | ||
| !./packages/*/tests/ | ||
| !./packages/**/*.test.* |
There was a problem hiding this comment.
We could generate this glob automatically by looping through the workspaces field in package.json. But yes I agree that probably don't need to worry about this for v1.
| with: | ||
| name: preview-build-artifacts | ||
|
|
||
| - name: Resolve GitHub tools ref |
There was a problem hiding this comment.
Is this step necessary? I see that for instance check-changelog reads github.action_ref.


Add a reusable
workflow_callworkflow so consumer repos can replace their copy-pastedpublish-preview.ymlwith a thin wrapper.New files
.github/workflows/publish-preview.yml— Reusable workflow with 4 jobs: fork check, comment reaction, build (no token), publish (with token). Supports both monorepos and polyrepos viais-monorepoinput..github/scripts/generate-preview-build-message.sh— Generates the PR comment (collapsible package list for monorepos, single install command for polyrepos)..github/actions/prepare-preview-builds/— Composite action for monorepo manifest preparation (from prior commit).Consumer example