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
13 changes: 11 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -354,11 +354,18 @@ jobs:
state_file="$state_dir/publish-state-${owner_sanitised}-${repo_sanitised}-${cwd_hash}-${workspace_prefix}${version_component}.json"
mkdir -p "$state_dir"
printf %s "$payload" > "$state_file"
# Craft writes each completed target to this inode, then unlinks
# the state file on full success. Keep its final contents for the
# issue updater without changing the file Craft reads and writes.
issue_state_file="$state_file.issue"
ln -- "$state_file" "$issue_state_file"
echo "state_file=$state_file" >> "$GITHUB_OUTPUT"
echo "issue_state_file=$issue_state_file" >> "$GITHUB_OUTPUT"
echo "Wrote state file: $state_file"

- uses: docker://getsentry/craft:latest
name: Publish using Craft
id: craft-publish
with:
entrypoint: /bin/bash
args: >-
Expand Down Expand Up @@ -418,10 +425,12 @@ jobs:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

- name: Update completed targets and remove label
if: ${{ cancelled() || failure() }}
if: ${{ always() }}
Comment thread
BYK marked this conversation as resolved.
Comment thread
BYK marked this conversation as resolved.
# Bookkeeping errors must not turn a completed release into a failed publish.
continue-on-error: ${{ steps.craft-publish.outcome == 'success' }}
env:
PUBLISH_ARGS: ${{ steps.inputs.outputs.result }}
CRAFT_STATE_FILE_PATH: ${{ steps.craft-state.outputs.state_file }}
CRAFT_STATE_FILE_PATH: ${{ steps.craft-state.outputs.issue_state_file }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node .__publish__/src/publish/update-issue.js

Expand Down
74 changes: 68 additions & 6 deletions src/modules/__tests__/publish-workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ function getSetTargetsScript() {
return section.groups.script.replace(/^ {10}/gm, "");
}

function runSetTargets({ path, repo, version, workspace = "" }) {
function runSetTargets({
path,
repo,
version,
workspace = "",
targets = ["github"],
}) {
const directory = mkdtempSync(join(tmpdir(), "publish-workflow-test-"));
temporaryDirectories.push(directory);
const output = join(directory, "github-output");
Expand All @@ -38,7 +44,7 @@ function runSetTargets({ path, repo, version, workspace = "" }) {
...process.env,
CRAFT_PUBLISH_PATH: path,
CRAFT_PUBLISH_REPO: repo,
CRAFT_PUBLISH_TARGETS_JSON: '["github"]',
CRAFT_PUBLISH_TARGETS_JSON: JSON.stringify(targets),
CRAFT_PUBLISH_VERSION: version,
CRAFT_PUBLISH_WORKSPACE: workspace,
GITHUB_OUTPUT: output,
Expand All @@ -48,10 +54,10 @@ function runSetTargets({ path, repo, version, workspace = "" }) {
});

expect(result.status, result.stderr).toBe(0);
const stateFile = readFileSync(output, "utf8")
.trim()
.replace("state_file=", "");
return { stateFile, state: readFileSync(stateFile, "utf8") };
const outputs = readFileSync(output, "utf8");
const stateFile = outputs.match(/^state_file=(.+)$/m)?.[1];
const issueStateFile = outputs.match(/^issue_state_file=(.+)$/m)?.[1];
return { stateFile, issueStateFile, state: readFileSync(stateFile, "utf8") };
}

afterEach(() => {
Expand Down Expand Up @@ -116,6 +122,62 @@ test("publishes only on a fresh CI-ready label event", () => {
);
});

test("records completed targets before closing a successful publish issue", () => {
const workflow = getWorkflow();
const publish = workflow.indexOf("name: Publish using Craft");
const update = workflow.indexOf(
"name: Update completed targets and remove label"
);
const close = workflow.indexOf("name: Close on success");
const publishStep = workflow.slice(publish, update);
const updateStep = workflow.slice(update, close);

expect(update).toBeGreaterThan(publish);
expect(close).toBeGreaterThan(update);
expect(publishStep).toContain("id: craft-publish");
expect(updateStep).toContain("if: ${{ always() }}");
expect(updateStep).toContain(
"continue-on-error: ${{ steps.craft-publish.outcome == 'success' }}"
);
expect(updateStep).toContain(
"CRAFT_STATE_FILE_PATH: ${{ steps.craft-state.outputs.issue_state_file }}"
);
expect(updateStep).toContain(
"run: node .__publish__/src/publish/update-issue.js"
);
expect(workflow.slice(close)).toContain("if: ${{ success() }}");
});

test("creates a state file when no targets were already published", () => {
const { stateFile, state } = runSetTargets({
path: ".",
repo: "craft",
version: "2.34.1",
targets: [],
});

expect(stateFile).toMatch(
/\.craft-state\/craft\/publish-state-getsentry-craft-c232c383e26f-2\.34\.1\.json$/
);
expect(JSON.parse(state)).toEqual({ published: {} });
});

test("keeps Craft's final state after a successful publish removes its state file", () => {
const { stateFile, issueStateFile } = runSetTargets({
path: ".",
repo: "craft",
version: "2.34.1",
targets: [],
});
const finalState = { published: { npm: true, gcs: true } };

writeFileSync(stateFile, JSON.stringify(finalState));
expect(JSON.parse(readFileSync(issueStateFile, "utf8"))).toEqual(finalState);

rmSync(stateFile);
expect(JSON.parse(readFileSync(issueStateFile, "utf8"))).toEqual(finalState);
});

test("uses Craft's legacy root state filename", () => {
const { stateFile, state } = runSetTargets({
path: ".",
Expand Down
Loading