Skip to content

docs(agents): add submit --wait, --check-only, and --json flags #59

docs(agents): add submit --wait, --check-only, and --json flags

docs(agents): add submit --wait, --check-only, and --json flags #59

# Publish Packages — publishes @recallnet/* packages to npmjs via trusted
# publishing, mirrors them to GitHub Packages, and creates package-scoped
# GitHub Releases. This workflow must be the direct publishing workflow
# configured in npmjs, so it runs on pushes to main instead of chaining off
# another workflow.
name: Publish Packages
on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
release:
# Trusted publishing binds npm packages to this exact workflow file. Keep
# the job enabled for version-commit reruns so partial publish failures can
# be resumed after Changesets has already consumed the pending files.
if: github.repository == 'recallnet/codecontext'
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
- uses: pnpm/action-setup@v2
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm for trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check for changesets
id: changesets
run: |
output=$(pnpm changeset status 2>&1) && rc=0 || rc=$?
if [ "$rc" -eq 0 ]; then
echo "has_changesets=true" >> "$GITHUB_OUTPUT"
echo "Found pending changesets"
elif echo "$output" | grep -qi "no.*changesets"; then
echo "has_changesets=false" >> "$GITHUB_OUTPUT"
echo "No pending changesets"
else
echo "::error::changeset status failed unexpectedly:"
echo "$output"
exit 1
fi
- name: Determine release mode
id: release_mode
env:
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
if [ "${{ steps.changesets.outputs.has_changesets }}" = "true" ]; then
echo "mode=fresh" >> "$GITHUB_OUTPUT"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
echo "Release mode: fresh changeset publish"
elif [ "$HEAD_COMMIT_MESSAGE" = "chore: version packages [skip ci]" ]; then
echo "mode=recovery" >> "$GITHUB_OUTPUT"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
echo "Release mode: recovery from version commit"
else
echo "mode=none" >> "$GITHUB_OUTPUT"
echo "should_publish=false" >> "$GITHUB_OUTPUT"
echo "Release mode: nothing to publish"
fi
- name: Build packages
if: steps.release_mode.outputs.should_publish == 'true'
run: pnpm build
- name: Version packages
if: steps.release_mode.outputs.mode == 'fresh'
run: pnpm changeset version
- name: Prepare release manifest
if: steps.release_mode.outputs.should_publish == 'true'
id: releases
env:
RELEASE_MODE: ${{ steps.release_mode.outputs.mode }}
run: |
# @context decision: Publish and release steps run from an explicit
# package/version manifest instead of live Changesets state so reruns
# can resume after the version commit has already consumed the
# changeset files. Without this, npmjs-success/GitHub-Packages-failure
# leaves the mirror permanently skipped on retry.
#
# @context risk: New packages may not exist in the base revision.
# Treat a missing previous package.json as "new package" rather than a
# hard failure so first-release automation works.
#
# @context decision: GitHub Releases are package-scoped rather than
# repo-scoped because a single publish run can ship multiple npm
# packages. Collapsing that into one repo release would hide which
# package/version pairs actually shipped.
node <<'EOF'
const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');
const mode = process.env.RELEASE_MODE;
const outputPath = path.join(process.env.RUNNER_TEMP, 'published-releases.json');
const fileArgs =
mode === 'fresh'
? ['diff', '--name-only', '--', 'packages/*/package.json']
: ['diff', '--name-only', 'HEAD^', 'HEAD', '--', 'packages/*/package.json'];
const diff = execFileSync('git', fileArgs, { encoding: 'utf8' })
.trim()
.split('\n')
.filter(Boolean);
const baseRef = mode === 'fresh' ? 'HEAD' : 'HEAD^';
const releases = [];
for (const file of diff) {
const after = JSON.parse(fs.readFileSync(file, 'utf8'));
if (!after.name || after.private) {
continue;
}
let before = null;
try {
before = JSON.parse(execFileSync('git', ['show', `${baseRef}:${file}`], { encoding: 'utf8' }));
} catch (error) {
if (error.status !== 128) {
throw error;
}
}
if (before?.version === after.version) {
continue;
}
const shortName = after.name.replace(/^@[^/]+\//, '');
releases.push({
packageName: after.name,
version: after.version,
directory: path.dirname(file),
tag: `${shortName}-v${after.version}`,
npmUrl: `https://www.npmjs.com/package/${after.name}/v/${after.version}`,
ghRegistry: 'https://npm.pkg.github.com',
});
}
fs.writeFileSync(outputPath, JSON.stringify(releases, null, 2) + '\n');
fs.appendFileSync(
process.env.GITHUB_OUTPUT,
`release_count=${releases.length}\nrelease_manifest=${outputPath}\n`,
);
EOF
- name: Commit and push version changes
if: steps.release_mode.outputs.mode == 'fresh'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "chore: version packages [skip ci]" || echo "No changes to commit"
git push
- name: Publish packages to npmjs
if: >-
steps.release_mode.outputs.should_publish == 'true' &&
steps.releases.outputs.release_count != '0'
env:
NPM_CONFIG_PROVENANCE: "true"
RELEASE_MANIFEST: ${{ steps.releases.outputs.release_manifest }}
run: |
node <<'EOF'
const fs = require('node:fs');
const { execFileSync } = require('node:child_process');
const releases = JSON.parse(fs.readFileSync(process.env.RELEASE_MANIFEST, 'utf8'));
for (const release of releases) {
const spec = `${release.packageName}@${release.version}`;
let published = false;
try {
const existing = execFileSync(
'npm',
['view', spec, 'version', '--registry=https://registry.npmjs.org'],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
).trim();
published = existing === release.version;
} catch (error) {
if (error.status !== 1) {
throw error;
}
}
if (published) {
console.log(`npmjs already has ${spec}; skipping publish`);
continue;
}
execFileSync(
'npm',
['publish', '.', '--provenance', '--registry=https://registry.npmjs.org'],
{ cwd: release.directory, stdio: 'inherit' },
);
}
EOF
- name: Configure GitHub Packages auth
if: >-
steps.release_mode.outputs.should_publish == 'true' &&
steps.releases.outputs.release_count != '0'
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: "https://npm.pkg.github.com"
scope: "@recallnet"
- name: Publish packages to GitHub Packages
if: >-
steps.release_mode.outputs.should_publish == 'true' &&
steps.releases.outputs.release_count != '0'
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: "false"
RELEASE_MANIFEST: ${{ steps.releases.outputs.release_manifest }}
run: |
node <<'EOF'
const fs = require('node:fs');
const { execFileSync } = require('node:child_process');
const releases = JSON.parse(fs.readFileSync(process.env.RELEASE_MANIFEST, 'utf8'));
for (const release of releases) {
const spec = `${release.packageName}@${release.version}`;
let published = false;
try {
const existing = execFileSync(
'npm',
['view', spec, 'version', '--registry=https://npm.pkg.github.com'],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
).trim();
published = existing === release.version;
} catch (error) {
if (error.status !== 1) {
throw error;
}
}
if (published) {
console.log(`GitHub Packages already has ${spec}; skipping publish`);
continue;
}
execFileSync(
'npm',
['publish', '.', '--registry=https://npm.pkg.github.com'],
{ cwd: release.directory, stdio: 'inherit' },
);
}
EOF
- name: Create GitHub Releases
if: >-
steps.release_mode.outputs.should_publish == 'true' &&
steps.releases.outputs.release_count != '0'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_MANIFEST: ${{ steps.releases.outputs.release_manifest }}
run: |
release_target=$(git rev-parse HEAD)
node <<'EOF' > "$RUNNER_TEMP/release-lines.tsv"
const fs = require('node:fs');
const releases = JSON.parse(fs.readFileSync(process.env.RELEASE_MANIFEST, 'utf8'));
for (const release of releases) {
const body = [
`${release.packageName} ${release.version} was published to npmjs and mirrored to GitHub Packages from this workflow run.`,
'',
`npm: ${release.npmUrl}`,
`github-packages-registry: ${release.ghRegistry}`,
].join('\n');
process.stdout.write([
release.tag,
release.packageName,
release.version,
Buffer.from(body, 'utf8').toString('base64'),
].join('\t') + '\n');
}
EOF
while IFS=$'\t' read -r tag package_name version body_b64; do
[ -n "$tag" ] || continue
body=$(printf '%s' "$body_b64" | base64 --decode)
if gh release view "$tag" >/dev/null 2>&1; then
gh release edit "$tag" \
--title "$package_name@$version" \
--notes "$body"
else
gh release create "$tag" \
--target "$release_target" \
--title "$package_name@$version" \
--notes "$body"
fi
done < "$RUNNER_TEMP/release-lines.tsv"